First draft of auto-Swagger
This commit is contained in:
parent
99d0d0055e
commit
6d7921e8b4
8 changed files with 205 additions and 21 deletions
|
|
@ -168,4 +168,8 @@ def cleanup():
|
||||||
atexit.register(cleanup)
|
atexit.register(cleanup)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port="5000", threaded=True, debug=True, use_reloader=False)
|
# app.run(host="0.0.0.0", port="5000", threaded=True, debug=True, use_reloader=False)
|
||||||
|
#from pprint import pprint
|
||||||
|
#pprint(labthing.spec.to_dict())
|
||||||
|
with open('spec.yaml', 'w') as f:
|
||||||
|
f.write(labthing.spec.to_yaml())
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
||||||
from openflexure_microscope.common.flask_labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
from openflexure_microscope.common.flask_labthings.find import find_device
|
from openflexure_microscope.common.flask_labthings.find import find_device
|
||||||
from openflexure_microscope.common.flask_labthings.decorators import use_args, marshal_with
|
from openflexure_microscope.common.flask_labthings.decorators import use_args, marshal_with, doc
|
||||||
from openflexure_microscope.common.flask_labthings import fields
|
from openflexure_microscope.common.flask_labthings import fields
|
||||||
from openflexure_microscope.utilities import filter_dict
|
from openflexure_microscope.utilities import filter_dict
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ from openflexure_microscope.api.v2.views.captures import capture_schema
|
||||||
import logging
|
import logging
|
||||||
from flask import jsonify, request, abort, url_for, redirect, send_file
|
from flask import jsonify, request, abort, url_for, redirect, send_file
|
||||||
|
|
||||||
|
@doc(tags=["actions"])
|
||||||
class CaptureAPI(Resource):
|
class CaptureAPI(Resource):
|
||||||
"""
|
"""
|
||||||
Create a new image capture.
|
Create a new image capture.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ class marshal_with(object):
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
|
|
||||||
def __call__(self, f):
|
def __call__(self, f):
|
||||||
|
# Pass params to call function attribute for external access
|
||||||
|
f.__apispec__ = f.__dict__.get('__apispec__', {})
|
||||||
|
f.__apispec__["_schema"] = self.schema
|
||||||
|
# Wrapper function
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
resp = f(*args, **kwargs)
|
resp = f(*args, **kwargs)
|
||||||
|
|
@ -46,27 +50,37 @@ class marshal_with(object):
|
||||||
|
|
||||||
|
|
||||||
class use_args(object):
|
class use_args(object):
|
||||||
def __init__(self, argmap, **kwargs):
|
def __init__(self, schema, **kwargs):
|
||||||
"""
|
"""
|
||||||
Equivalent to webargs.flask_parser.use_args
|
Equivalent to webargs.flask_parser.use_args
|
||||||
"""
|
"""
|
||||||
self.argmap = argmap
|
self.schema = schema
|
||||||
self.wrapper = flaskparser.use_args(argmap, **kwargs)
|
self.wrapper = flaskparser.use_args(schema, **kwargs)
|
||||||
|
|
||||||
def __call__(self, f):
|
def __call__(self, f):
|
||||||
|
# Pass params to call function attribute for external access
|
||||||
|
f.__apispec__ = f.__dict__.get('__apispec__', {})
|
||||||
|
f.__apispec__["_params"] = self.schema
|
||||||
|
# Wrapper function
|
||||||
update_wrapper(self.wrapper, f)
|
update_wrapper(self.wrapper, f)
|
||||||
return self.wrapper(f)
|
return self.wrapper(f)
|
||||||
|
|
||||||
|
|
||||||
class use_kwargs(object):
|
class use_kwargs(use_args):
|
||||||
def __init__(self, argmap, **kwargs):
|
def __init__(self, schema, **kwargs):
|
||||||
"""
|
"""
|
||||||
Equivalent to webargs.flask_parser.use_kwargs
|
Equivalent to webargs.flask_parser.use_kwargs
|
||||||
"""
|
"""
|
||||||
kwargs["as_kwargs"] = True
|
kwargs["as_kwargs"] = True
|
||||||
self.argmap = argmap
|
use_args.__init__(self, schema, **kwargs)
|
||||||
self.wrapper = flaskparser.use_args(argmap, **kwargs)
|
|
||||||
|
|
||||||
|
class doc(object):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def __call__(self, f):
|
def __call__(self, f):
|
||||||
update_wrapper(self.wrapper, f)
|
# Pass params to call function attribute for external access
|
||||||
return self.wrapper(f)
|
f.__apispec__ = f.__dict__.get('__apispec__', {})
|
||||||
|
f.__apispec__.update(self.kwargs)
|
||||||
|
return f
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
from flask import url_for, jsonify
|
from flask import url_for, jsonify
|
||||||
|
from apispec import APISpec
|
||||||
|
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||||
|
|
||||||
from .plugins import BasePlugin
|
from .plugins import BasePlugin
|
||||||
from .views.plugins import PluginListResource
|
from .views.plugins import PluginListResource
|
||||||
from .views.tasks import TaskList, TaskResource
|
from .views.tasks import TaskList, TaskResource
|
||||||
|
|
||||||
|
from .spec import view2path
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
||||||
from .exceptions import JSONExceptionHandler
|
from .exceptions import JSONExceptionHandler
|
||||||
|
|
||||||
from . import EXTENSION_NAME
|
from . import EXTENSION_NAME
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
class LabThing(object):
|
class LabThing(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -17,6 +22,7 @@ class LabThing(object):
|
||||||
prefix: str = "",
|
prefix: str = "",
|
||||||
title: str = "",
|
title: str = "",
|
||||||
description: str = "",
|
description: str = "",
|
||||||
|
version: str = "0.0.0",
|
||||||
handle_errors: bool = True,
|
handle_errors: bool = True,
|
||||||
):
|
):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
@ -32,17 +38,53 @@ class LabThing(object):
|
||||||
self.endpoints = set()
|
self.endpoints = set()
|
||||||
|
|
||||||
self.url_prefix = prefix
|
self.url_prefix = prefix
|
||||||
self.description = description
|
self._description = description
|
||||||
self.title = title
|
self._title = title
|
||||||
|
self._version = version
|
||||||
|
|
||||||
if handle_errors:
|
if handle_errors:
|
||||||
self.error_handler = JSONExceptionHandler()
|
self.error_handler = JSONExceptionHandler()
|
||||||
else:
|
else:
|
||||||
self.error_handler = None
|
self.error_handler = None
|
||||||
|
|
||||||
|
self.spec = APISpec(
|
||||||
|
title=self.title,
|
||||||
|
version=self.version,
|
||||||
|
openapi_version="3.0.2",
|
||||||
|
plugins=[MarshmallowPlugin()],
|
||||||
|
)
|
||||||
|
|
||||||
if app is not None:
|
if app is not None:
|
||||||
self.init_app(app)
|
self.init_app(app)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self, ):
|
||||||
|
return self._description
|
||||||
|
|
||||||
|
@description.setter
|
||||||
|
def description(self, description: str):
|
||||||
|
self._description = description
|
||||||
|
self.spec.description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self, ):
|
||||||
|
return self._title
|
||||||
|
|
||||||
|
@title.setter
|
||||||
|
def title(self, title: str):
|
||||||
|
self._title = title
|
||||||
|
self.spec.title = title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self, ):
|
||||||
|
return str(self._version)
|
||||||
|
|
||||||
|
@version.setter
|
||||||
|
def version(self, version: str):
|
||||||
|
self._version = version
|
||||||
|
self.spec.version = version
|
||||||
|
|
||||||
|
|
||||||
### Flask stuff
|
### Flask stuff
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
|
|
@ -56,14 +98,14 @@ class LabThing(object):
|
||||||
if self.error_handler:
|
if self.error_handler:
|
||||||
self.error_handler.init_app(self.app)
|
self.error_handler.init_app(self.app)
|
||||||
|
|
||||||
# Create base routes
|
|
||||||
self._create_base_routes()
|
|
||||||
|
|
||||||
# Add resources, if registered before tying to a Flask app
|
# Add resources, if registered before tying to a Flask app
|
||||||
if len(self.resources) > 0:
|
if len(self.resources) > 0:
|
||||||
for resource, urls, endpoint, kwargs in self.resources:
|
for resource, urls, endpoint, kwargs in self.resources:
|
||||||
self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs)
|
self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs)
|
||||||
|
|
||||||
|
# Create base routes
|
||||||
|
self._create_base_routes()
|
||||||
|
|
||||||
def teardown(self, exception):
|
def teardown(self, exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -158,10 +200,13 @@ class LabThing(object):
|
||||||
api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
|
api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
|
||||||
"""
|
"""
|
||||||
endpoint = endpoint or resource.__name__.lower()
|
endpoint = endpoint or resource.__name__.lower()
|
||||||
|
|
||||||
|
logging.debug(f"{endpoint}: {type(resource)}")
|
||||||
|
|
||||||
if self.app is not None:
|
if self.app is not None:
|
||||||
self._register_view(self.app, resource, *urls, endpoint=endpoint, **kwargs)
|
self._register_view(self.app, resource, *urls, endpoint=endpoint, **kwargs)
|
||||||
else:
|
|
||||||
self.resources.append((resource, urls, endpoint, kwargs))
|
self.resources.append((resource, urls, endpoint, kwargs))
|
||||||
|
|
||||||
def resource(self, *urls, **kwargs):
|
def resource(self, *urls, **kwargs):
|
||||||
"""Wraps a :class:`~flask_restful.Resource` class, adding it to the
|
"""Wraps a :class:`~flask_restful.Resource` class, adding it to the
|
||||||
|
|
@ -208,6 +253,10 @@ class LabThing(object):
|
||||||
rule = self._complete_url(url, "")
|
rule = self._complete_url(url, "")
|
||||||
# Add the url to the application or blueprint
|
# Add the url to the application or blueprint
|
||||||
app.add_url_rule(rule, view_func=resource_func, **kwargs)
|
app.add_url_rule(rule, view_func=resource_func, **kwargs)
|
||||||
|
# Add the resource to our API spec
|
||||||
|
self.spec.path(
|
||||||
|
**view2path(rule, resource, self.spec)
|
||||||
|
)
|
||||||
|
|
||||||
### Utilities
|
### Utilities
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,19 @@ class Resource(MethodView):
|
||||||
"""Currently identical to MethodView
|
"""Currently identical to MethodView
|
||||||
"""
|
"""
|
||||||
endpoint = None
|
endpoint = None
|
||||||
|
methods = ["get", "post", "put", "delete"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
MethodView.__init__(self, *args, **kwargs)
|
MethodView.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def doc(self):
|
||||||
|
docs = {"operations": {}}
|
||||||
|
if hasattr(self, "__apispec__"):
|
||||||
|
docs.update(self.__apispec__)
|
||||||
|
|
||||||
|
|
||||||
|
for meth in Resource.methods:
|
||||||
|
if hasattr(self, meth) and hasattr(getattr(self, meth), "__apispec__"):
|
||||||
|
docs["operations"][meth] = {}
|
||||||
|
docs["operations"][meth] = getattr(self, meth).__apispec__
|
||||||
|
return docs
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import flask
|
import flask
|
||||||
import marshmallow
|
import marshmallow
|
||||||
|
|
||||||
_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()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ class Schema(marshmallow.Schema):
|
||||||
"""
|
"""
|
||||||
if many is sentinel:
|
if many is sentinel:
|
||||||
many = self.many
|
many = self.many
|
||||||
if _MARSHMALLOW_VERSION_INFO[0] >= 3:
|
if MARSHMALLOW_VERSION_INFO[0] >= 3:
|
||||||
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
|
||||||
|
|
|
||||||
89
openflexure_microscope/common/flask_labthings/spec.py
Normal file
89
openflexure_microscope/common/flask_labthings/spec.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
|
||||||
|
from .resource import Resource
|
||||||
|
from .utilities import rupdate
|
||||||
|
from apispec import APISpec
|
||||||
|
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||||
|
|
||||||
|
from .fields import Field
|
||||||
|
from marshmallow import Schema as BaseSchema
|
||||||
|
|
||||||
|
from collections import Mapping
|
||||||
|
|
||||||
|
def view2path(rule: str, view: Resource, spec: APISpec):
|
||||||
|
params = {
|
||||||
|
"path": rule, # TODO: Validate this slightly (leading / etc)
|
||||||
|
"operations": view2operations(view, spec),
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasattr(view, "__apispec__"):
|
||||||
|
# Recursively update params
|
||||||
|
rupdate(params, view.__apispec__)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
def view2operations(view: Resource, spec: APISpec):
|
||||||
|
ops = {}
|
||||||
|
for method in Resource.methods:
|
||||||
|
if hasattr(view, method):
|
||||||
|
ops[method] = {}
|
||||||
|
if hasattr(getattr(view, method), "__apispec__"):
|
||||||
|
ops[method] = doc2operation(getattr(view, method).__apispec__, spec)
|
||||||
|
|
||||||
|
return ops
|
||||||
|
|
||||||
|
|
||||||
|
def doc2operation(apispec: dict, spec: APISpec):
|
||||||
|
op = {}
|
||||||
|
if "_params" in apispec:
|
||||||
|
op["requestBody"] = {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": convert_schema(apispec.get("_params"), spec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if "_schema" in apispec:
|
||||||
|
op["responses"] = {
|
||||||
|
200: {
|
||||||
|
"description": "success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": convert_schema(apispec.get("_schema"), spec)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val in apispec.items():
|
||||||
|
if not key in ["_params", "_schema"]:
|
||||||
|
op[key] = val
|
||||||
|
|
||||||
|
return op
|
||||||
|
|
||||||
|
|
||||||
|
def convert_schema(schema, spec: APISpec):
|
||||||
|
if isinstance(schema, BaseSchema):
|
||||||
|
return schema
|
||||||
|
elif isinstance(schema, Mapping):
|
||||||
|
return map2properties(schema, spec)
|
||||||
|
else:
|
||||||
|
raise TypeError("Unsupported schema type. Ensure schema is a Schema class, or dictionary of Field objects")
|
||||||
|
|
||||||
|
def map2properties(schema, spec: APISpec):
|
||||||
|
marshmallow_plugin = next(
|
||||||
|
plugin for plugin in spec.plugins
|
||||||
|
if isinstance(plugin, MarshmallowPlugin)
|
||||||
|
)
|
||||||
|
converter = marshmallow_plugin.converter
|
||||||
|
|
||||||
|
d = {}
|
||||||
|
for k, v in schema.items():
|
||||||
|
if isinstance(v, Field):
|
||||||
|
d[k] = converter.field2property(v)
|
||||||
|
elif isinstance(v, Mapping):
|
||||||
|
d[k] = map2properties(v, spec)
|
||||||
|
else:
|
||||||
|
d[k] = v
|
||||||
|
|
||||||
|
return {"properties": d}
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
||||||
|
from .schema import Schema, marshmallow, MARSHMALLOW_VERSION_INFO
|
||||||
|
import collections.abc
|
||||||
|
|
||||||
|
from webargs import dict2schema as wa_dict2schema
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
def description_from_view(view_class):
|
def description_from_view(view_class):
|
||||||
|
|
@ -11,3 +17,12 @@ def description_from_view(view_class):
|
||||||
d = {"methods": methods, "description": brief_description}
|
d = {"methods": methods, "description": brief_description}
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def rupdate(d, u):
|
||||||
|
for k, v in u.items():
|
||||||
|
if isinstance(v, collections.abc.Mapping):
|
||||||
|
d[k] = rupdate(d.get(k, {}), v)
|
||||||
|
else:
|
||||||
|
d[k] = v
|
||||||
|
return d
|
||||||
Loading…
Add table
Add a link
Reference in a new issue