Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring REST responses #446

Merged
merged 5 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 13 additions & 32 deletions src/orion/serving/experiments_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
:synopsis: Serves all the requests made to experiments/ REST endpoint
"""
import json
from typing import Optional

from falcon import Request, Response

from orion.core.worker.experiment import Experiment
from orion.core.worker.trial import Trial
from orion.serving.parameters import retrieve_experiment, verify_query_parameters
from orion.serving.responses import build_trial_response
from orion.serving.responses import build_experiment_response, build_experiments_response
from orion.storage.base import get_storage


Expand All @@ -28,14 +30,8 @@ def on_get(self, req: Request, resp: Response):
experiments = self.storage.fetch_experiments({})
leaf_experiments = _find_latest_versions(experiments)

result = []
for name, version in leaf_experiments.items():
result.append({
'name': name,
'version': version
})

resp.body = json.dumps(result)
response = build_experiments_response(leaf_experiments)
resp.body = json.dumps(response)

def on_get_experiment(self, req: Request, resp: Response, name: str):
"""
Expand All @@ -44,26 +40,13 @@ def on_get_experiment(self, req: Request, resp: Response, name: str):
"""
verify_query_parameters(req.params, ['version'])
version = req.get_param_as_int('version')

experiment = retrieve_experiment(name, version)
response = {
"name": experiment.name,
"version": experiment.version,
"status": _retrieve_status(experiment),
"trialsCompleted": experiment.stats['trials_completed'] if experiment.stats else 0,
"startTime": str(experiment.stats['start_time']) if experiment.stats else None,
"endTime": str(experiment.stats['finish_time']) if experiment.stats else None,
"user": experiment.metadata['user'],
"orionVersion": experiment.metadata['orion_version'],
"config": {
"maxTrials": experiment.max_trials,
"poolSize": experiment.pool_size,
"algorithm": _retrieve_algorithm(experiment),
"space": experiment.configuration['space']
},
"bestTrial": _retrieve_best_trial(experiment)
}

status = _retrieve_status(experiment)
algorithm = _retrieve_algorithm(experiment)
best_trial = _retrieve_best_trial(experiment)

response = build_experiment_response(experiment, status, algorithm, best_trial)
resp.body = json.dumps(response)


Expand Down Expand Up @@ -101,11 +84,9 @@ def _retrieve_algorithm(experiment: Experiment) -> dict:
return result


def _retrieve_best_trial(experiment: Experiment) -> dict:
def _retrieve_best_trial(experiment: Experiment) -> Optional[Trial]:
"""Constructs the view of the best trial if there is one"""
if not experiment.stats:
return {}

trial = experiment.get_trial(uid=experiment.stats['best_trials_id'])
return None

return build_trial_response(trial)
return experiment.get_trial(uid=experiment.stats['best_trials_id'])
16 changes: 9 additions & 7 deletions src/orion/serving/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from orion.core.utils.exceptions import NoConfigurationError
from orion.core.worker.experiment import Experiment
from orion.core.worker.trial import Trial
from orion.serving.responses import ERROR_EXPERIMENT_NOT_FOUND, ERROR_INVALID_PARAMETER, \
ERROR_TRIAL_NOT_FOUND


def verify_query_parameters(parameters: dict, supported_parameters: list):
Expand All @@ -35,7 +37,7 @@ def verify_query_parameters(parameters: dict, supported_parameters: list):
for parameter in parameters:
if parameter not in supported_parameters:
description = _compose_error_message(parameter, supported_parameters)
raise falcon.HTTPBadRequest('Invalid parameter', description)
raise falcon.HTTPBadRequest(ERROR_INVALID_PARAMETER, description)


def verify_status(status):
Expand All @@ -45,7 +47,7 @@ def verify_status(status):
description += 'The value of the parameter must be one of {}'.format(
list(Trial.allowed_stati))

raise falcon.HTTPBadRequest('Invalid parameter', description)
raise falcon.HTTPBadRequest(ERROR_INVALID_PARAMETER, description)


def _compose_error_message(key: str, supported_parameters: list):
Expand Down Expand Up @@ -74,15 +76,15 @@ def retrieve_experiment(experiment_name: str, version: int = None) -> Optional[E
experiment = experiment_builder.build_view(experiment_name, version)
if version and experiment.version != version:
raise falcon.HTTPNotFound(
title='Experiment Not Found',
title=ERROR_EXPERIMENT_NOT_FOUND,
description=f'Experiment "{experiment_name}" has no version "{version}"')
return experiment
except NoConfigurationError:
raise falcon.HTTPNotFound(title='Experiment not found',
raise falcon.HTTPNotFound(title=ERROR_EXPERIMENT_NOT_FOUND,
description=f'Experiment "{experiment_name}" does not exist')


def retrieve_trial(experiment, trial_id):
def retrieve_trial(experiment: Experiment, trial_id: str):
"""
Retrieves the trial for the given id in the experiment

Expand All @@ -91,7 +93,7 @@ def retrieve_trial(experiment, trial_id):
experiment: Experiment
The experiment containing the trial.

trial_id: int
trial_id: str
The id of the trial

Raises
Expand All @@ -102,6 +104,6 @@ def retrieve_trial(experiment, trial_id):
"""
trial = experiment.get_trial(uid=trial_id)
if not trial:
raise falcon.HTTPNotFound(title='Trial not found',
raise falcon.HTTPNotFound(title=ERROR_TRIAL_NOT_FOUND,
description=f'Trial "{trial_id}" does not exist')
return trial
97 changes: 94 additions & 3 deletions src/orion/serving/responses.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# -*- coding: utf-8 -*-
"""
:mod:`orion.serving.responses` -- Build JSON-compatible responses for Orion objects
===================================================================================
:mod:`orion.serving.responses` -- Helpers for building responses according to the specification
===============================================================================================

.. module:: responses
:platform: Unix
:synopsis: Build JSON-compatible responses for Orion objects
:synopsis: Offers functions and attributes to generate response objects according
to the API specification
"""
from orion.core.worker.experiment import Experiment
from orion.core.worker.trial import Trial

ERROR_EXPERIMENT_NOT_FOUND = 'Experiment not found'
ERROR_INVALID_PARAMETER = 'Invalid parameter'
ERROR_TRIAL_NOT_FOUND = 'Trial not found'


def build_trial_response(trial: Trial) -> dict:
"""
Expand All @@ -31,3 +37,88 @@ def build_trial_response(trial: Trial) -> dict:
'parameters': trial.params,
'objective': trial.objective.value,
'statistics': {statistic.name: statistic.value for statistic in trial.statistics}}


def build_experiment_response(experiment: Experiment,
status: str,
algorithm: dict,
best_trial: Trial = None):
"""
Build the response representing an experiment response object according to the API
specification.

Parameters
----------
experiment: Experiment
The experiment to return to the API
status: str
The status of the experiment
algorithm: dict
The dictionary containing the algorithm's configuration
best_trial: Trial (Optional)
The best trial to date of the experiment

Returns
-------
A JSON-serializable experiment response object representing the given experiment.
"""
return {
"name": experiment.name,
"version": experiment.version,
"status": status,
"trialsCompleted": experiment.stats['trials_completed'] if experiment.stats else 0,
"startTime": str(experiment.stats['start_time']) if experiment.stats else None,
"endTime": str(experiment.stats['finish_time']) if experiment.stats else None,
"user": experiment.metadata['user'],
"orionVersion": experiment.metadata['orion_version'],
"config": {
"maxTrials": experiment.max_trials,
"poolSize": experiment.pool_size,
"algorithm": algorithm,
"space": experiment.configuration['space']
},
"bestTrial": build_trial_response(best_trial) if best_trial else {}
}


def build_experiments_response(experiments: dict):
"""
Build the response representing a list of experiments according to the API specification.

Parameters
----------
experiments: dict
A dict containing pairs of ``experiment-name:experiment-version``.

Returns
-------
A JSON-serializable list of experiments as defined in the API specification.
"""
result = []
for name, version in experiments.items():
result.append({
'name': name,
'version': version
})
return result


def build_trials_response(trials: list):
"""
Build the response representing a list of trials according to the API specification.

Parameters
----------
trials: list
A list of :class:`orion.core.worker.trial.Trial`.

Returns
-------
A JSON-serializable list of trials as defined in the API specification.
"""
response = []
for trial in trials:
response.append({
'id': trial.id
})
return response
12 changes: 4 additions & 8 deletions src/orion/serving/trials_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from orion.serving.parameters import retrieve_experiment, retrieve_trial, verify_query_parameters, \
verify_status
from orion.serving.responses import build_trial_response
from orion.serving.responses import build_trial_response, build_trials_response
from orion.storage.base import get_storage

SUPPORTED_PARAMETERS = ['ancestors', 'status', 'version']
Expand Down Expand Up @@ -43,12 +43,7 @@ def on_get_trials_in_experiment(self, req: Request, resp: Response, experiment_n
else:
trials = experiment.fetch_trials(with_ancestors)

response = []
for trial in trials:
response.append({
'id': trial.id
})

response = build_trials_response(trials)
resp.body = json.dumps(response)

def on_get_trial_in_experiment(self, req: Request, resp: Response, experiment_name: str,
Expand All @@ -60,4 +55,5 @@ def on_get_trial_in_experiment(self, req: Request, resp: Response, experiment_na
experiment = retrieve_experiment(experiment_name)
trial = retrieve_trial(experiment, trial_id)

resp.body = json.dumps(build_trial_response(trial))
response = build_trial_response(trial)
resp.body = json.dumps(response)
2 changes: 1 addition & 1 deletion tests/functional/serving/test_trials_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def test_trials_for_specific_version(self, client):

assert response.status == "404 Not Found"
assert response.json == {
'title': 'Experiment Not Found',
'title': 'Experiment not found',
'description': 'Experiment "a" has no version "4"'
}

Expand Down