Better handle response documentation

This commit is contained in:
jtc42 2020-01-06 22:28:00 +00:00
parent 1ccdad8a86
commit 8afb0757bd
12 changed files with 143 additions and 115 deletions

View file

@ -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,
}
}
},
)

View file

@ -13,6 +13,7 @@ from openflexure_microscope.common.labthings_core.utilities import (
snake_to_spine,
)
class BaseExtension:
"""
Parent class for all extensions.

View file

@ -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

View file

@ -0,0 +1,3 @@
TASK_ENDPOINT = "labthing_task"
TASK_LIST_ENDPOINT = "labthing_task_list"
EXTENSION_LIST_ENDPOINT = "labthing_extension_list"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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):
"""