Cleaned code structure

This commit is contained in:
jtc42 2020-01-06 16:45:38 +00:00
parent 79abc3fee3
commit 70149e9732
20 changed files with 196 additions and 332 deletions

View file

@ -2,8 +2,10 @@ from webargs import flaskparser
from functools import wraps, update_wrapper
from flask import make_response
from .utilities import rupdate
from .spec import update_spec
from openflexure_microscope.common.labthings_core.utilities import rupdate
from openflexure_microscope.common.labthings_core.spec import update_spec
from openflexure_microscope.common.labthings_core.decorators import doc, doc_response
def unpack(value):
@ -74,27 +76,3 @@ class use_kwargs(use_args):
"""
kwargs["as_kwargs"] = True
use_args.__init__(self, schema, **kwargs)
class doc(object):
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, f):
# Pass params to call function attribute for external access
update_spec(f, self.kwargs)
return f
class doc_response(object):
def __init__(self, code, description, **kwargs):
self.code = code
self.description = description
self.kwargs = kwargs
def __call__(self, f):
# Pass params to call function attribute for external access
f.__apispec__ = f.__dict__.get("__apispec__", {})
d = {"responses": {self.code: {"description": self.description, **self.kwargs}}}
rupdate(f.__apispec__, d)
return f

View file

@ -7,8 +7,14 @@ import sys
import os
import glob
from openflexure_microscope.common.labthings_core.utilities import get_docstring
from openflexure_microscope.utilities import camel_to_snake, snake_to_spine
from openflexure_microscope.common.labthings_core.utilities import (
get_docstring,
camel_to_snake,
snake_to_spine,
)
# TODO: Move into core?
class BaseExtension:
@ -134,7 +140,7 @@ def find_instances_in_module(module, class_to_find):
return objs
def find_extensions_in_file(extension_path, module_name="extensions"):
def find_extensions_in_file(extension_path: str, module_name="extensions"):
logging.debug(f"Loading extensions from {extension_path}")
spec = util.spec_from_file_location(module_name, extension_path)
@ -149,7 +155,7 @@ def find_extensions_in_file(extension_path, module_name="extensions"):
return find_instances_in_module(mod, BaseExtension)
def find_extensions(extension_dir, module_name="extensions"):
def find_extensions(extension_dir: str, module_name="extensions"):
logging.debug(f"Loading extensions from {extension_dir}")
extensions = []

View file

@ -1,175 +1 @@
from marshmallow.fields import *
from marshmallow import missing
import re
from flask import url_for
from flask.views import View
_tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*")
def _tpl(val):
"""Return value within ``< >`` if possible, else return ``None``."""
match = _tpl_pattern.match(val)
if match:
return match.groups()[0]
return None
def _get_value(obj, key, default=missing):
"""Slightly-modified version of marshmallow.utils.get_value.
If a dot-delimited ``key`` is passed and any attribute in the
path is `None`, return `None`.
"""
if "." in key:
return _get_value_for_keys(obj, key.split("."), default)
else:
return _get_value_for_key(obj, key, default)
def _get_value_for_keys(obj, keys, default):
if len(keys) == 1:
return _get_value_for_key(obj, keys[0], default)
else:
value = _get_value_for_key(obj, keys[0], default)
# XXX This differs from the marshmallow implementation
if value is None:
return None
return _get_value_for_keys(value, keys[1:], default)
def _get_value_for_key(obj, key, default):
if not hasattr(obj, "__getitem__"):
return getattr(obj, key, default)
try:
return obj[key]
except (KeyError, IndexError, TypeError, AttributeError):
return getattr(obj, key, default)
class URLFor(Field):
"""Field that outputs the URL for an endpoint. Acts identically to
Flask's ``url_for`` function, except that arguments can be pulled from the
object to be serialized.
Usage: ::
url = URLFor('author_get', id='<id>')
https_url = URLFor('author_get', id='<id>', _scheme='https', _external=True)
:param str endpoint: Flask endpoint name.
:param kwargs: Same keyword arguments as Flask's url_for, except string
arguments enclosed in `< >` will be interpreted as attributes to pull
from the object.
"""
_CHECK_ATTRIBUTE = False
def __init__(self, endpoint, **kwargs):
# Handle the case where endpoint is an attached flask View of any kind
if isinstance(endpoint, type) and issubclass(endpoint, View):
self.view_class = endpoint
self.endpoint = None
# Handle cases where endpoint is passed directly as a string
elif type(endpoint) == str:
self.view_class = None
self.endpoint = endpoint
else:
raise RuntimeError(
f"Endpoint {endpoint} is not a valid Flask view or endpoint string."
)
self.params = kwargs
Field.__init__(self, **kwargs)
def _serialize(self, value, key, obj):
"""Output the URL for the endpoint, given the kwargs passed to
``__init__``.
"""
# Get endpoint from view_class, if needed
if self.view_class and not self.endpoint:
if hasattr(self.view_class, "endpoint"):
self.endpoint = self.view_class.endpoint
else:
raise RuntimeError(
f"Resource {self.endpoint} has not been added to a LabThing application. Unable to generate URL."
)
# Generate URL for
param_values = {}
for name, attr_tpl in self.params.items():
attr_name = _tpl(str(attr_tpl))
if attr_name:
attribute_value = _get_value(obj, attr_name, default=missing)
if attribute_value is None:
return None
if attribute_value is not missing:
param_values[name] = attribute_value
else:
raise AttributeError(
"{attr_name!r} is not a valid "
"attribute of {obj!r}".format(attr_name=attr_name, obj=obj)
)
else:
param_values[name] = attr_tpl
return url_for(self.endpoint, **param_values)
UrlFor = URLFor
class AbsoluteURLFor(URLFor):
"""Field that outputs the absolute URL for an endpoint."""
def __init__(self, endpoint, **kwargs):
kwargs["_external"] = True
URLFor.__init__(self, endpoint=endpoint, **kwargs)
AbsoluteUrlFor = AbsoluteURLFor
def _rapply(d, func, *args, **kwargs):
"""Apply a function to all values in a dictionary or list of dictionaries, recursively."""
if isinstance(d, (tuple, list)):
return [_rapply(each, func, *args, **kwargs) for each in d]
if isinstance(d, dict):
return {key: _rapply(value, func, *args, **kwargs) for key, value in d.items()}
else:
return func(d, *args, **kwargs)
def _url_val(val, key, obj, **kwargs):
"""Function applied by `HyperlinksField` to get the correct value in the
schema.
"""
if isinstance(val, URLFor):
return val.serialize(key, obj, **kwargs)
else:
return val
class Hyperlinks(Field):
"""Field that outputs a dictionary of hyperlinks,
given a dictionary schema with :class:`~flask_marshmallow.fields.URLFor`
objects as values.
Example: ::
_links = Hyperlinks({
'self': URLFor('author', id='<id>'),
'collection': URLFor('author_list'),
})
`URLFor` objects can be nested within the dictionary. ::
_links = Hyperlinks({
'self': {
'href': URLFor('book', id='<id>'),
'title': 'book detail'
}
})
:param dict schema: A dict that maps names to
:class:`~fields.URLFor` fields.
"""
_CHECK_ATTRIBUTE = False
def __init__(self, schema, **kwargs):
self.schema = schema
Field.__init__(self, **kwargs)
def _serialize(self, value, attr, obj):
return _rapply(self.schema, _url_val, key=attr, obj=obj)
from openflexure_microscope.common.labthings_core.fields import *

View file

@ -3,14 +3,13 @@ from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from .extensions import BaseExtension
from .views.extensions import ExtensionListResource
from .views.tasks import TaskList, TaskResource
from .spec import view2path
from .utilities import description_from_view
from .views.extensions import ExtensionList
from .views.tasks import TaskList, TaskResource
from openflexure_microscope.common.labthings_core.utilities import get_docstring
from openflexure_microscope.common.labthings_core.spec import view2path
from . import EXTENSION_NAME
@ -114,8 +113,8 @@ class LabThing(object):
)
# Add extension overview
self.add_resource(ExtensionListResource, "/extensions")
self.register_property(ExtensionListResource)
self.add_resource(ExtensionList, "/extensions")
self.register_property(ExtensionList)
# Add task routes
self.add_resource(TaskList, "/tasks")
self.register_property(TaskList)
@ -320,8 +319,8 @@ class LabThing(object):
"methods": ["GET"],
},
"extensions": {
"href": self.url_for(ExtensionListResource, _external=True),
**description_from_view(ExtensionListResource),
"href": self.url_for(ExtensionList, _external=True),
**description_from_view(ExtensionList),
},
"tasks": {
"href": self.url_for(TaskList, _external=True),

View file

@ -1,23 +1,12 @@
from flask.views import MethodView
from openflexure_microscope.common.labthings_core.resource import BaseResource
class Resource(MethodView):
class Resource(MethodView, BaseResource):
"""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

View file

@ -1,39 +1 @@
# -*- coding: utf-8 -*-
import flask
import marshmallow
MARSHMALLOW_VERSION_INFO = tuple(
[int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
)
sentinel = object()
class Schema(marshmallow.Schema):
"""Base serializer with which to define custom serializers.
See `marshmallow.Schema` for more details about the `Schema` API.
"""
def jsonify(self, obj, many=sentinel, *args, **kwargs):
"""Return a JSON response containing the serialized data.
:param obj: Object to serialize.
:param bool many: Whether `obj` should be serialized as an instance
or as a collection. If unset, defaults to the value of the
`many` attribute on this Schema.
:param kwargs: Additional keyword arguments passed to `flask.jsonify`.
.. versionchanged:: 0.6.0
Takes the same arguments as `marshmallow.Schema.dump`. Additional
keyword arguments are passed to `flask.jsonify`.
.. versionchanged:: 0.6.3
The `many` argument for this method defaults to the value of
the `many` attribute on the Schema. Previously, the `many`
argument of this method defaulted to False, regardless of the
value of `Schema.many`.
"""
if many is sentinel:
many = self.many
if MARSHMALLOW_VERSION_INFO[0] >= 3:
data = self.dump(obj, many=many)
else:
data = self.dump(obj, many=many).data
return flask.jsonify(data, *args, **kwargs)
from openflexure_microscope.common.labthings_core.schema import *

View file

@ -1,137 +0,0 @@
from .resource import Resource
from .utilities import rupdate
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from openflexure_microscope.common.labthings_core.utilities import (
get_docstring,
get_summary,
)
from .fields import Field
from marshmallow import Schema as BaseSchema
from collections import Mapping
def update_spec(obj, spec):
obj.__apispec__ = obj.__dict__.get("__apispec__", {})
rupdate(obj.__apispec__, spec)
return obj.__apispec__
def view2path(rule: str, view: Resource, spec: APISpec):
params = {
"path": rule, # TODO: Validate this slightly (leading / etc)
"operations": view2operations(view, spec),
"description": get_docstring(view),
"summary": get_summary(view),
}
if hasattr(view, "__apispec__"):
# Recursively update params
rupdate(params, view.__apispec__)
return params
def view2operations(view: Resource, spec: APISpec, populate_default: bool = True):
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] = {}
rupdate(
ops[method],
{
"description": get_docstring(getattr(view, method)),
"summary": get_summary(getattr(view, method)),
},
)
if hasattr(getattr(view, method), "__apispec__"):
rupdate(
ops[method], doc2operation(getattr(view, method).__apispec__, spec)
)
return ops
def doc2operation(apispec: dict, spec: APISpec):
op = {}
if "_params" in apispec:
rupdate(
op,
{
"requestBody": {
"content": {
"application/json": {
"schema": convert_schema(apispec.get("_params"), spec)
}
}
}
},
)
if "_schema" in apispec:
rupdate(
op,
{
"responses": {
200: {
"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}

View file

@ -2,10 +2,6 @@ from openflexure_microscope.common.labthings_core.utilities import (
get_docstring,
get_summary,
)
from .schema import Schema, marshmallow, MARSHMALLOW_VERSION_INFO
import collections.abc
import logging
def description_from_view(view_class):
@ -18,14 +14,3 @@ def description_from_view(view_class):
d = {"methods": methods, "description": summary}
return d
def rupdate(d, u):
for k, v in u.items():
if isinstance(v, collections.abc.Mapping):
if not k in d:
d[k] = {}
d[k] = rupdate(d.get(k, {}), v)
else:
d[k] = v
return d

View file

@ -3,13 +3,15 @@ Top-level representation of attached and enabled Extensions
"""
from openflexure_microscope.common.labthings_core.utilities import get_docstring
from openflexure_microscope.common.labthings_core.schema import Schema
from openflexure_microscope.common.labthings_core import fields
from ..utilities import description_from_view
from openflexure_microscope.common.flask_labthings.resource import Resource
from openflexure_microscope.common.flask_labthings.find import registered_extensions
from openflexure_microscope.common.flask_labthings.schema import Schema
from openflexure_microscope.common.flask_labthings.decorators import marshal_with
from openflexure_microscope.common.flask_labthings import fields
from ..resource import Resource
from ..find import registered_extensions
from ..decorators import marshal_with
from marshmallow import pre_dump
from flask import jsonify, url_for
@ -48,7 +50,7 @@ class ExtensionSchema(Schema):
return data
class ExtensionListResource(Resource):
class ExtensionList(Resource):
"""
List and basic documentation for all enabled Extensions
"""

View file

@ -1,14 +1,15 @@
from flask import abort, url_for
from openflexure_microscope.common.flask_labthings.schema import Schema
from openflexure_microscope.common.flask_labthings.decorators import marshal_with
from openflexure_microscope.common.flask_labthings import fields
from openflexure_microscope.common.labthings_core import tasks
from openflexure_microscope.common.flask_labthings.resource import Resource
from openflexure_microscope.common.flask_labthings.utilities import (
description_from_view,
)
from openflexure_microscope.common.labthings_core import tasks
from openflexure_microscope.common.labthings_core.schema import Schema
from openflexure_microscope.common.labthings_core import fields
from marshmallow import pre_dump