diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py index c86733f4..dd9f4986 100644 --- a/openflexure_microscope/api/default_extensions/scan.py +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -10,18 +10,14 @@ from openflexure_microscope.common.flask_labthings.find import ( find_extension, ) from openflexure_microscope.common.flask_labthings.extensions import BaseExtension -from openflexure_microscope.common.flask_labthings.views.tasks import TaskSchema from openflexure_microscope.common.flask_labthings.decorators import ( + marshal_task, marshal_with, use_args, ) from openflexure_microscope.common.flask_labthings import fields -from openflexure_microscope.devel import ( - taskify, - abort, - update_task_progress, -) +from openflexure_microscope.devel import taskify, abort, update_task_progress from openflexure_microscope.common.flask_labthings.resource import Resource import time @@ -361,7 +357,7 @@ class TileScanAPI(Resource): "resize": fields.Dict(missing=None), # TODO: Validate keys } ) - @marshal_with(TaskSchema()) + @marshal_task def post(self, args): microscope = find_device("openflexure_microscope") diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py index d4d8c804..0f1735d9 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities.py @@ -2,7 +2,11 @@ import logging import os import errno from werkzeug.exceptions import BadRequest -from flask import url_for, Blueprint +from flask import url_for, Blueprint, current_app + + +def view_class_from_endpoint(endpoint: str): + return current_app.view_functions[endpoint].view_class def blueprint_for_module(module_name, api_ver=2, suffix=""): diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py index 900f3f17..bcb3cd20 100644 --- a/openflexure_microscope/api/v2/views/actions/camera.py +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -35,7 +35,7 @@ class CaptureAPI(Resource): } ) @marshal_with(capture_schema) - @doc_response(200, "Capture successful") + @doc_response(200, description="Capture successful") def post(self, args): """ Create a new capture diff --git a/openflexure_microscope/common/flask_labthings/decorators.py b/openflexure_microscope/common/flask_labthings/decorators.py index a7748f04..d3aafa16 100644 --- a/openflexure_microscope/common/flask_labthings/decorators.py +++ b/openflexure_microscope/common/flask_labthings/decorators.py @@ -1,10 +1,12 @@ from webargs import flaskparser from functools import wraps, update_wrapper from flask import make_response +from http import HTTPStatus from openflexure_microscope.common.labthings_core.utilities import rupdate from .spec import update_spec +from .schema import TaskSchema def unpack(value): @@ -28,23 +30,23 @@ def unpack(value): class marshal_with(object): - def __init__(self, schema): + def __init__(self, schema, code=200): """ :param schema: a dict of whose keys will make up the final serialized response output """ self.schema = schema + self.code = code def __call__(self, f): # Pass params to call function attribute for external access - update_spec(f, {"_schema": self.schema}) + update_spec(f, {"_schema": {self.code: self.schema}}) # Wrapper function @wraps(f) def wrapper(*args, **kwargs): resp = f(*args, **kwargs) if isinstance(resp, tuple): data, code, headers = unpack(resp) - print((data, code, headers)) return make_response(self.schema.jsonify(data), code, headers) else: return make_response(self.schema.jsonify(resp)) @@ -52,6 +54,23 @@ class marshal_with(object): return wrapper +def marshal_task(f): + # Pass params to call function attribute for external access + update_spec(f, {"responses": {201: {"description": "Task started successfully"}}}) + update_spec(f, {"_schema": {201: TaskSchema()}}) + # Wrapper function + @wraps(f) + def wrapper(*args, **kwargs): + resp = f(*args, **kwargs) + if isinstance(resp, tuple): + data, code, headers = unpack(resp) + return make_response(TaskSchema().jsonify(data), code, headers) + else: + return make_response(TaskSchema().jsonify(resp)) + + return wrapper + + class use_args(object): def __init__(self, schema, **kwargs): """ @@ -88,7 +107,7 @@ class doc(object): class doc_response(object): - def __init__(self, code, description, **kwargs): + def __init__(self, code, description=None, **kwargs): self.code = code self.description = description self.kwargs = kwargs @@ -99,7 +118,10 @@ class doc_response(object): f, { "responses": { - self.code: {"description": self.description, **self.kwargs} + self.code: { + "description": self.description or HTTPStatus(self.code).phrase, + **self.kwargs, + } } }, ) diff --git a/openflexure_microscope/common/flask_labthings/extensions.py b/openflexure_microscope/common/flask_labthings/extensions.py index 9fa66652..538609df 100644 --- a/openflexure_microscope/common/flask_labthings/extensions.py +++ b/openflexure_microscope/common/flask_labthings/extensions.py @@ -13,6 +13,7 @@ from openflexure_microscope.common.labthings_core.utilities import ( snake_to_spine, ) + class BaseExtension: """ Parent class for all extensions. diff --git a/openflexure_microscope/common/flask_labthings/labthing.py b/openflexure_microscope/common/flask_labthings/labthing.py index b090ef2f..029ba64f 100644 --- a/openflexure_microscope/common/flask_labthings/labthing.py +++ b/openflexure_microscope/common/flask_labthings/labthing.py @@ -2,7 +2,8 @@ from flask import url_for, jsonify from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin -from . import EXTENSION_NAME +from . import EXTENSION_NAME # TODO: Move into .names +from .names import TASK_ENDPOINT, TASK_LIST_ENDPOINT, EXTENSION_LIST_ENDPOINT from .extensions import BaseExtension from .utilities import description_from_view from .spec import view2path @@ -112,12 +113,14 @@ class LabThing(object): ) # Add extension overview - self.add_resource(ExtensionList, "/extensions") + self.add_resource( + ExtensionList, "/extensions", endpoint=EXTENSION_LIST_ENDPOINT + ) self.register_property(ExtensionList) # Add task routes - self.add_resource(TaskList, "/tasks") + self.add_resource(TaskList, "/tasks", endpoint=TASK_LIST_ENDPOINT) self.register_property(TaskList) - self.add_resource(TaskResource, "/tasks/") + self.add_resource(TaskResource, "/tasks/", endpoint=TASK_ENDPOINT) ### Device stuff diff --git a/openflexure_microscope/common/flask_labthings/names.py b/openflexure_microscope/common/flask_labthings/names.py new file mode 100644 index 00000000..417c5138 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/names.py @@ -0,0 +1,3 @@ +TASK_ENDPOINT = "labthing_task" +TASK_LIST_ENDPOINT = "labthing_task_list" +EXTENSION_LIST_ENDPOINT = "labthing_extension_list" diff --git a/openflexure_microscope/common/flask_labthings/schema.py b/openflexure_microscope/common/flask_labthings/schema.py index 83294a0e..bc2691b5 100644 --- a/openflexure_microscope/common/flask_labthings/schema.py +++ b/openflexure_microscope/common/flask_labthings/schema.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- -import flask +from flask import jsonify, url_for import marshmallow +from .names import TASK_ENDPOINT, TASK_LIST_ENDPOINT, EXTENSION_LIST_ENDPOINT +from .utilities import view_class_from_endpoint, description_from_view +from . import fields + MARSHMALLOW_VERSION_INFO = tuple( [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] ) @@ -36,4 +40,56 @@ class Schema(marshmallow.Schema): data = self.dump(obj, many=many) else: data = self.dump(obj, many=many).data - return flask.jsonify(data, *args, **kwargs) + return jsonify(data, *args, **kwargs) + + +class TaskSchema(Schema): + _ID = fields.String(data_key="id") + target_string = fields.String(data_key="function") + _status = fields.String(data_key="status") + progress = fields.String() + data = fields.Raw() + _return_value = fields.Raw(data_key="return") + _start_time = fields.String(data_key="start_time") + _end_time = fields.String(data_key="end_time") + + links = fields.Dict() + + @marshmallow.pre_dump + def generate_links(self, data, **kwargs): + data.links = { + "self": { + "href": url_for(TASK_ENDPOINT, id=data.id, _external=True), + "mimetype": "application/json", + **description_from_view(view_class_from_endpoint(TASK_ENDPOINT)), + } + } + return data + + +class ExtensionSchema(Schema): + name = fields.String(data_key="title") + _name_python_safe = fields.String(data_key="pythonName") + _cls = fields.String(data_key="pythonObject") + gui = fields.Dict() + description = fields.String() + + links = fields.Dict() + + @marshmallow.pre_dump + def generate_links(self, data, **kwargs): + d = {} + for view_id, view_data in data.views.items(): + view_cls = view_data["view"] + view_kwargs = view_data["kwargs"] + view_rule = view_data["rule"] + # Make links dictionary if it doesn't yet exist + d[view_id] = { + "href": url_for(EXTENSION_LIST_ENDPOINT, **view_kwargs, _external=True) + + view_rule, + **description_from_view(view_cls), + } + + data.links = d + + return data diff --git a/openflexure_microscope/common/flask_labthings/spec.py b/openflexure_microscope/common/flask_labthings/spec.py index fa8c3190..2ccfc042 100644 --- a/openflexure_microscope/common/flask_labthings/spec.py +++ b/openflexure_microscope/common/flask_labthings/spec.py @@ -12,6 +12,7 @@ from .fields import Field from marshmallow import Schema as BaseSchema from collections import Mapping +from http import HTTPStatus def update_spec(obj, spec): @@ -35,23 +36,11 @@ def view2path(rule: str, view: Resource, spec: APISpec): return params -def view2operations(view: Resource, spec: APISpec, populate_default: bool = True): +def view2operations(view: Resource, spec: APISpec): ops = {} for method in Resource.methods: if hasattr(view, method): - # Populate with default responses - if populate_default: - ops[method] = { - "responses": { - 200: { - "description": get_summary(getattr(view, method)) - or "Success" - }, - 404: {"description": "Resource not found"}, - } - } - else: - ops[method] = {} + ops[method] = {} rupdate( ops[method], @@ -61,15 +50,17 @@ def view2operations(view: Resource, spec: APISpec, populate_default: bool = True }, ) - if hasattr(getattr(view, method), "__apispec__"): - rupdate( - ops[method], doc2operation(getattr(view, method).__apispec__, spec) - ) + rupdate(ops[method], method2operation(getattr(view, method), spec)) return ops -def doc2operation(apispec: dict, spec: APISpec): +def method2operation(method: callable, spec: APISpec): + if hasattr(method, "__apispec__"): + apispec = getattr(method, "__apispec__") + else: + apispec = {} + op = {} if "_params" in apispec: rupdate( @@ -86,24 +77,37 @@ def doc2operation(apispec: dict, spec: APISpec): ) if "_schema" in apispec: + for code, schema in apispec.get("_schema", {}).items(): + rupdate( + op, + { + "responses": { + code: { + "description": HTTPStatus(code).phrase, + "content": { + "application/json": { + "schema": convert_schema(schema, spec) + } + }, + } + } + }, + ) + else: + # If no explicit responses are known, populate with defaults rupdate( op, { "responses": { - 200: { - "content": { - "application/json": { - "schema": convert_schema(apispec.get("_schema"), spec) - } - } - } + 200: {"description": get_summary(method) or HTTPStatus(200).phrase} } }, ) + # Bung in any extra swagger fields supplied for key, val in apispec.items(): if not key in ["_params", "_schema"]: - op[key] = val + rupdate(op, {key: val}) return op diff --git a/openflexure_microscope/common/flask_labthings/utilities.py b/openflexure_microscope/common/flask_labthings/utilities.py index a0b152e5..e06dad4c 100644 --- a/openflexure_microscope/common/flask_labthings/utilities.py +++ b/openflexure_microscope/common/flask_labthings/utilities.py @@ -2,6 +2,7 @@ from openflexure_microscope.common.labthings_core.utilities import ( get_docstring, get_summary, ) +from flask import current_app def description_from_view(view_class): @@ -14,3 +15,7 @@ def description_from_view(view_class): d = {"methods": methods, "description": summary} return d + + +def view_class_from_endpoint(endpoint: str): + return current_app.view_functions[endpoint].view_class diff --git a/openflexure_microscope/common/flask_labthings/views/extensions.py b/openflexure_microscope/common/flask_labthings/views/extensions.py index 947fa6cb..364c061d 100644 --- a/openflexure_microscope/common/flask_labthings/views/extensions.py +++ b/openflexure_microscope/common/flask_labthings/views/extensions.py @@ -6,46 +6,10 @@ from openflexure_microscope.common.flask_labthings.utilities import get_docstrin from openflexure_microscope.common.flask_labthings.schema import Schema from openflexure_microscope.common.flask_labthings import fields -from ..utilities import description_from_view - from ..resource import Resource from ..find import registered_extensions - +from ..schema import ExtensionSchema from ..decorators import marshal_with -from marshmallow import pre_dump - -from flask import jsonify, url_for - -import logging - - -class ExtensionSchema(Schema): - name = fields.String(data_key="title") - _name_python_safe = fields.String(data_key="pythonName") - _cls = fields.String(data_key="pythonObject") - gui = fields.Dict() - description = fields.String() - - links = fields.Dict() - - # TODO: Automate this somewhat - @pre_dump - def generate_links(self, data, **kwargs): - d = {} - for view_id, view_data in data.views.items(): - view_cls = view_data["view"] - view_kwargs = view_data["kwargs"] - view_rule = view_data["rule"] - # Make links dictionary if it doesn't yet exist - d[view_id] = { - "href": url_for(ExtensionList.endpoint, **view_kwargs, _external=True) - + view_rule, - **description_from_view(view_cls), - } - - data.links = d - - return data class ExtensionList(Resource): diff --git a/openflexure_microscope/common/flask_labthings/views/tasks.py b/openflexure_microscope/common/flask_labthings/views/tasks.py index 75bae06f..2a203c23 100644 --- a/openflexure_microscope/common/flask_labthings/views/tasks.py +++ b/openflexure_microscope/common/flask_labthings/views/tasks.py @@ -2,40 +2,10 @@ from flask import abort, url_for from ..decorators import marshal_with from ..resource import Resource -from ..schema import Schema -from .. import fields -from ..utilities import description_from_view -from ..spec import update_spec +from ..schema import TaskSchema from openflexure_microscope.common.labthings_core import tasks -from marshmallow import pre_dump - - -class TaskSchema(Schema): - _ID = fields.String(data_key="id") - target_string = fields.String(data_key="function") - _status = fields.String(data_key="status") - progress = fields.String() - data = fields.Raw() - _return_value = fields.Raw(data_key="return") - _start_time = fields.String(data_key="start_time") - _end_time = fields.String(data_key="end_time") - - links = fields.Dict() - - # TODO: Automate this somewhat - @pre_dump - def generate_links(self, data, **kwargs): - data.links = { - "self": { - "href": url_for(TaskResource.endpoint, id=data.id, _external=True), - "mimetype": "application/json", - **description_from_view(TaskResource), - } - } - return data - class TaskList(Resource): """