Better handle response documentation
This commit is contained in:
parent
1ccdad8a86
commit
8afb0757bd
12 changed files with 143 additions and 115 deletions
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from openflexure_microscope.common.labthings_core.utilities import (
|
|||
snake_to_spine,
|
||||
)
|
||||
|
||||
|
||||
class BaseExtension:
|
||||
"""
|
||||
Parent class for all extensions.
|
||||
|
|
|
|||
|
|
@ -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/<id>")
|
||||
self.add_resource(TaskResource, "/tasks/<id>", endpoint=TASK_ENDPOINT)
|
||||
|
||||
### 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 -*-
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue