Better handle response documentation
This commit is contained in:
parent
1ccdad8a86
commit
8afb0757bd
12 changed files with 143 additions and 115 deletions
|
|
@ -10,18 +10,14 @@ from openflexure_microscope.common.flask_labthings.find import (
|
||||||
find_extension,
|
find_extension,
|
||||||
)
|
)
|
||||||
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
|
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 (
|
from openflexure_microscope.common.flask_labthings.decorators import (
|
||||||
|
marshal_task,
|
||||||
marshal_with,
|
marshal_with,
|
||||||
use_args,
|
use_args,
|
||||||
)
|
)
|
||||||
from openflexure_microscope.common.flask_labthings import fields
|
from openflexure_microscope.common.flask_labthings import fields
|
||||||
|
|
||||||
from openflexure_microscope.devel import (
|
from openflexure_microscope.devel import taskify, abort, update_task_progress
|
||||||
taskify,
|
|
||||||
abort,
|
|
||||||
update_task_progress,
|
|
||||||
)
|
|
||||||
|
|
||||||
from openflexure_microscope.common.flask_labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
import time
|
import time
|
||||||
|
|
@ -361,7 +357,7 @@ class TileScanAPI(Resource):
|
||||||
"resize": fields.Dict(missing=None), # TODO: Validate keys
|
"resize": fields.Dict(missing=None), # TODO: Validate keys
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@marshal_with(TaskSchema())
|
@marshal_task
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
microscope = find_device("openflexure_microscope")
|
microscope = find_device("openflexure_microscope")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@ import logging
|
||||||
import os
|
import os
|
||||||
import errno
|
import errno
|
||||||
from werkzeug.exceptions import BadRequest
|
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=""):
|
def blueprint_for_module(module_name, api_ver=2, suffix=""):
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class CaptureAPI(Resource):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@marshal_with(capture_schema)
|
@marshal_with(capture_schema)
|
||||||
@doc_response(200, "Capture successful")
|
@doc_response(200, description="Capture successful")
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
Create a new capture
|
Create a new capture
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
from webargs import flaskparser
|
from webargs import flaskparser
|
||||||
from functools import wraps, update_wrapper
|
from functools import wraps, update_wrapper
|
||||||
from flask import make_response
|
from flask import make_response
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings_core.utilities import rupdate
|
from openflexure_microscope.common.labthings_core.utilities import rupdate
|
||||||
|
|
||||||
from .spec import update_spec
|
from .spec import update_spec
|
||||||
|
from .schema import TaskSchema
|
||||||
|
|
||||||
|
|
||||||
def unpack(value):
|
def unpack(value):
|
||||||
|
|
@ -28,23 +30,23 @@ def unpack(value):
|
||||||
|
|
||||||
|
|
||||||
class marshal_with(object):
|
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
|
:param schema: a dict of whose keys will make up the final
|
||||||
serialized response output
|
serialized response output
|
||||||
"""
|
"""
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
|
self.code = code
|
||||||
|
|
||||||
def __call__(self, f):
|
def __call__(self, f):
|
||||||
# Pass params to call function attribute for external access
|
# 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
|
# Wrapper function
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
resp = f(*args, **kwargs)
|
resp = f(*args, **kwargs)
|
||||||
if isinstance(resp, tuple):
|
if isinstance(resp, tuple):
|
||||||
data, code, headers = unpack(resp)
|
data, code, headers = unpack(resp)
|
||||||
print((data, code, headers))
|
|
||||||
return make_response(self.schema.jsonify(data), code, headers)
|
return make_response(self.schema.jsonify(data), code, headers)
|
||||||
else:
|
else:
|
||||||
return make_response(self.schema.jsonify(resp))
|
return make_response(self.schema.jsonify(resp))
|
||||||
|
|
@ -52,6 +54,23 @@ class marshal_with(object):
|
||||||
return wrapper
|
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):
|
class use_args(object):
|
||||||
def __init__(self, schema, **kwargs):
|
def __init__(self, schema, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -88,7 +107,7 @@ class doc(object):
|
||||||
|
|
||||||
|
|
||||||
class doc_response(object):
|
class doc_response(object):
|
||||||
def __init__(self, code, description, **kwargs):
|
def __init__(self, code, description=None, **kwargs):
|
||||||
self.code = code
|
self.code = code
|
||||||
self.description = description
|
self.description = description
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
@ -99,7 +118,10 @@ class doc_response(object):
|
||||||
f,
|
f,
|
||||||
{
|
{
|
||||||
"responses": {
|
"responses": {
|
||||||
self.code: {"description": self.description, **self.kwargs}
|
self.code: {
|
||||||
|
"description": self.description or HTTPStatus(self.code).phrase,
|
||||||
|
**self.kwargs,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from openflexure_microscope.common.labthings_core.utilities import (
|
||||||
snake_to_spine,
|
snake_to_spine,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseExtension:
|
class BaseExtension:
|
||||||
"""
|
"""
|
||||||
Parent class for all extensions.
|
Parent class for all extensions.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ from flask import url_for, jsonify
|
||||||
from apispec import APISpec
|
from apispec import APISpec
|
||||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
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 .extensions import BaseExtension
|
||||||
from .utilities import description_from_view
|
from .utilities import description_from_view
|
||||||
from .spec import view2path
|
from .spec import view2path
|
||||||
|
|
@ -112,12 +113,14 @@ class LabThing(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add extension overview
|
# Add extension overview
|
||||||
self.add_resource(ExtensionList, "/extensions")
|
self.add_resource(
|
||||||
|
ExtensionList, "/extensions", endpoint=EXTENSION_LIST_ENDPOINT
|
||||||
|
)
|
||||||
self.register_property(ExtensionList)
|
self.register_property(ExtensionList)
|
||||||
# Add task routes
|
# Add task routes
|
||||||
self.add_resource(TaskList, "/tasks")
|
self.add_resource(TaskList, "/tasks", endpoint=TASK_LIST_ENDPOINT)
|
||||||
self.register_property(TaskList)
|
self.register_property(TaskList)
|
||||||
self.add_resource(TaskResource, "/tasks/<id>")
|
self.add_resource(TaskResource, "/tasks/<id>", endpoint=TASK_ENDPOINT)
|
||||||
|
|
||||||
### Device stuff
|
### Device stuff
|
||||||
|
|
||||||
|
|
|
||||||
3
openflexure_microscope/common/flask_labthings/names.py
Normal file
3
openflexure_microscope/common/flask_labthings/names.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
TASK_ENDPOINT = "labthing_task"
|
||||||
|
TASK_LIST_ENDPOINT = "labthing_task_list"
|
||||||
|
EXTENSION_LIST_ENDPOINT = "labthing_extension_list"
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import flask
|
from flask import jsonify, url_for
|
||||||
import marshmallow
|
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(
|
MARSHMALLOW_VERSION_INFO = tuple(
|
||||||
[int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
|
[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)
|
data = self.dump(obj, many=many)
|
||||||
else:
|
else:
|
||||||
data = self.dump(obj, many=many).data
|
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
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from .fields import Field
|
||||||
from marshmallow import Schema as BaseSchema
|
from marshmallow import Schema as BaseSchema
|
||||||
|
|
||||||
from collections import Mapping
|
from collections import Mapping
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
|
||||||
def update_spec(obj, spec):
|
def update_spec(obj, spec):
|
||||||
|
|
@ -35,23 +36,11 @@ def view2path(rule: str, view: Resource, spec: APISpec):
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def view2operations(view: Resource, spec: APISpec, populate_default: bool = True):
|
def view2operations(view: Resource, spec: APISpec):
|
||||||
ops = {}
|
ops = {}
|
||||||
for method in Resource.methods:
|
for method in Resource.methods:
|
||||||
if hasattr(view, method):
|
if hasattr(view, method):
|
||||||
# Populate with default responses
|
ops[method] = {}
|
||||||
if populate_default:
|
|
||||||
ops[method] = {
|
|
||||||
"responses": {
|
|
||||||
200: {
|
|
||||||
"description": get_summary(getattr(view, method))
|
|
||||||
or "Success"
|
|
||||||
},
|
|
||||||
404: {"description": "Resource not found"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
ops[method] = {}
|
|
||||||
|
|
||||||
rupdate(
|
rupdate(
|
||||||
ops[method],
|
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], method2operation(getattr(view, method), spec))
|
||||||
rupdate(
|
|
||||||
ops[method], doc2operation(getattr(view, method).__apispec__, spec)
|
|
||||||
)
|
|
||||||
|
|
||||||
return ops
|
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 = {}
|
op = {}
|
||||||
if "_params" in apispec:
|
if "_params" in apispec:
|
||||||
rupdate(
|
rupdate(
|
||||||
|
|
@ -86,24 +77,37 @@ def doc2operation(apispec: dict, spec: APISpec):
|
||||||
)
|
)
|
||||||
|
|
||||||
if "_schema" in 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(
|
rupdate(
|
||||||
op,
|
op,
|
||||||
{
|
{
|
||||||
"responses": {
|
"responses": {
|
||||||
200: {
|
200: {"description": get_summary(method) or HTTPStatus(200).phrase}
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": convert_schema(apispec.get("_schema"), spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Bung in any extra swagger fields supplied
|
||||||
for key, val in apispec.items():
|
for key, val in apispec.items():
|
||||||
if not key in ["_params", "_schema"]:
|
if not key in ["_params", "_schema"]:
|
||||||
op[key] = val
|
rupdate(op, {key: val})
|
||||||
|
|
||||||
return op
|
return op
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from openflexure_microscope.common.labthings_core.utilities import (
|
||||||
get_docstring,
|
get_docstring,
|
||||||
get_summary,
|
get_summary,
|
||||||
)
|
)
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
|
||||||
def description_from_view(view_class):
|
def description_from_view(view_class):
|
||||||
|
|
@ -14,3 +15,7 @@ def description_from_view(view_class):
|
||||||
d = {"methods": methods, "description": summary}
|
d = {"methods": methods, "description": summary}
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def view_class_from_endpoint(endpoint: str):
|
||||||
|
return current_app.view_functions[endpoint].view_class
|
||||||
|
|
|
||||||
|
|
@ -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.schema import Schema
|
||||||
from openflexure_microscope.common.flask_labthings import fields
|
from openflexure_microscope.common.flask_labthings import fields
|
||||||
|
|
||||||
from ..utilities import description_from_view
|
|
||||||
|
|
||||||
from ..resource import Resource
|
from ..resource import Resource
|
||||||
from ..find import registered_extensions
|
from ..find import registered_extensions
|
||||||
|
from ..schema import ExtensionSchema
|
||||||
from ..decorators import marshal_with
|
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):
|
class ExtensionList(Resource):
|
||||||
|
|
|
||||||
|
|
@ -2,40 +2,10 @@ from flask import abort, url_for
|
||||||
|
|
||||||
from ..decorators import marshal_with
|
from ..decorators import marshal_with
|
||||||
from ..resource import Resource
|
from ..resource import Resource
|
||||||
from ..schema import Schema
|
from ..schema import TaskSchema
|
||||||
from .. import fields
|
|
||||||
from ..utilities import description_from_view
|
|
||||||
from ..spec import update_spec
|
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings_core import tasks
|
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):
|
class TaskList(Resource):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue