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)
|
||||
|
||||
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.common.flask_labthings.resource import Resource
|
||||
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.utilities import filter_dict
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ from openflexure_microscope.api.v2.views.captures import capture_schema
|
|||
import logging
|
||||
from flask import jsonify, request, abort, url_for, redirect, send_file
|
||||
|
||||
|
||||
@doc(tags=["actions"])
|
||||
class CaptureAPI(Resource):
|
||||
"""
|
||||
Create a new image capture.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ class marshal_with(object):
|
|||
self.schema = schema
|
||||
|
||||
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)
|
||||
def wrapper(*args, **kwargs):
|
||||
resp = f(*args, **kwargs)
|
||||
|
|
@ -46,27 +50,37 @@ class marshal_with(object):
|
|||
|
||||
|
||||
class use_args(object):
|
||||
def __init__(self, argmap, **kwargs):
|
||||
def __init__(self, schema, **kwargs):
|
||||
"""
|
||||
Equivalent to webargs.flask_parser.use_args
|
||||
"""
|
||||
self.argmap = argmap
|
||||
self.wrapper = flaskparser.use_args(argmap, **kwargs)
|
||||
self.schema = schema
|
||||
self.wrapper = flaskparser.use_args(schema, **kwargs)
|
||||
|
||||
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)
|
||||
return self.wrapper(f)
|
||||
|
||||
|
||||
class use_kwargs(object):
|
||||
def __init__(self, argmap, **kwargs):
|
||||
class use_kwargs(use_args):
|
||||
def __init__(self, schema, **kwargs):
|
||||
"""
|
||||
Equivalent to webargs.flask_parser.use_kwargs
|
||||
"""
|
||||
kwargs["as_kwargs"] = True
|
||||
self.argmap = argmap
|
||||
self.wrapper = flaskparser.use_args(argmap, **kwargs)
|
||||
use_args.__init__(self, schema, **kwargs)
|
||||
|
||||
|
||||
class doc(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __call__(self, f):
|
||||
update_wrapper(self.wrapper, f)
|
||||
return self.wrapper(f)
|
||||
# Pass params to call function attribute for external access
|
||||
f.__apispec__ = f.__dict__.get('__apispec__', {})
|
||||
f.__apispec__.update(self.kwargs)
|
||||
return f
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
from flask import url_for, jsonify
|
||||
from apispec import APISpec
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
|
||||
from .plugins import BasePlugin
|
||||
from .views.plugins import PluginListResource
|
||||
from .views.tasks import TaskList, TaskResource
|
||||
|
||||
from .spec import view2path
|
||||
|
||||
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
||||
from .exceptions import JSONExceptionHandler
|
||||
|
||||
from . import EXTENSION_NAME
|
||||
|
||||
import logging
|
||||
|
||||
class LabThing(object):
|
||||
def __init__(
|
||||
|
|
@ -17,6 +22,7 @@ class LabThing(object):
|
|||
prefix: str = "",
|
||||
title: str = "",
|
||||
description: str = "",
|
||||
version: str = "0.0.0",
|
||||
handle_errors: bool = True,
|
||||
):
|
||||
self.app = app
|
||||
|
|
@ -32,17 +38,53 @@ class LabThing(object):
|
|||
self.endpoints = set()
|
||||
|
||||
self.url_prefix = prefix
|
||||
self.description = description
|
||||
self.title = title
|
||||
self._description = description
|
||||
self._title = title
|
||||
self._version = version
|
||||
|
||||
if handle_errors:
|
||||
self.error_handler = JSONExceptionHandler()
|
||||
else:
|
||||
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:
|
||||
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
|
||||
|
||||
def init_app(self, app):
|
||||
|
|
@ -56,14 +98,14 @@ class LabThing(object):
|
|||
if self.error_handler:
|
||||
self.error_handler.init_app(self.app)
|
||||
|
||||
# Create base routes
|
||||
self._create_base_routes()
|
||||
|
||||
# Add resources, if registered before tying to a Flask app
|
||||
if len(self.resources) > 0:
|
||||
for resource, urls, endpoint, kwargs in self.resources:
|
||||
self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs)
|
||||
|
||||
# Create base routes
|
||||
self._create_base_routes()
|
||||
|
||||
def teardown(self, exception):
|
||||
pass
|
||||
|
||||
|
|
@ -158,10 +200,13 @@ class LabThing(object):
|
|||
api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
|
||||
"""
|
||||
endpoint = endpoint or resource.__name__.lower()
|
||||
|
||||
logging.debug(f"{endpoint}: {type(resource)}")
|
||||
|
||||
if self.app is not None:
|
||||
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):
|
||||
"""Wraps a :class:`~flask_restful.Resource` class, adding it to the
|
||||
|
|
@ -208,6 +253,10 @@ class LabThing(object):
|
|||
rule = self._complete_url(url, "")
|
||||
# Add the url to the application or blueprint
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,19 @@ class Resource(MethodView):
|
|||
"""Currently identical to MethodView
|
||||
"""
|
||||
endpoint = None
|
||||
methods = ["get", "post", "put", "delete"]
|
||||
|
||||
def __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 marshmallow
|
||||
|
||||
_MARSHMALLOW_VERSION_INFO = tuple(
|
||||
MARSHMALLOW_VERSION_INFO = tuple(
|
||||
[int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
|
||||
)
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ class Schema(marshmallow.Schema):
|
|||
"""
|
||||
if many is sentinel:
|
||||
many = self.many
|
||||
if _MARSHMALLOW_VERSION_INFO[0] >= 3:
|
||||
if MARSHMALLOW_VERSION_INFO[0] >= 3:
|
||||
data = self.dump(obj, many=many)
|
||||
else:
|
||||
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 .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):
|
||||
|
|
@ -11,3 +17,12 @@ def description_from_view(view_class):
|
|||
d = {"methods": methods, "description": brief_description}
|
||||
|
||||
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