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

@ -7,6 +7,7 @@ from openflexure_microscope.common.flask_labthings.decorators import (
doc,
doc_response,
)
from openflexure_microscope.common.flask_labthings import fields
from openflexure_microscope.utilities import filter_dict

View file

@ -1,5 +1,10 @@
from openflexure_microscope.api.utilities import JsonResponse
from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path
from openflexure_microscope.common.labthings_core.utilities import (
get_by_path,
set_by_path,
create_from_path,
)
from openflexure_microscope.common.flask_labthings.find import find_device
from openflexure_microscope.common.flask_labthings.resource import Resource

View file

@ -1,5 +1,10 @@
from openflexure_microscope.api.utilities import gen, JsonResponse
from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path
from openflexure_microscope.common.labthings_core.utilities import (
get_by_path,
set_by_path,
create_from_path,
)
from openflexure_microscope.common.flask_labthings.find import find_device
from openflexure_microscope.common.flask_labthings.resource import Resource

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

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

View file

@ -0,0 +1,26 @@
from .spec import update_spec
from .utilities import rupdate
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

@ -0,0 +1 @@
from marshmallow.fields import *

View file

@ -0,0 +1,20 @@
class BaseResource:
"""
A LabThing Resource class should, regardless of the underlying framework, make use of
functions get(), put(), post(), and delete() corresponding to HTTP methods.
These functions will allow for automated documentation generation
"""
methods = ["get", "post", "put", "delete"]
def doc(self):
docs = {"operations": {}}
if hasattr(self, "__apispec__"):
docs.update(self.__apispec__)
for meth in BaseResource.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

@ -0,0 +1,39 @@
# -*- 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)

View file

@ -1,4 +1,4 @@
from .resource import Resource
from .resource import BaseResource
from .utilities import rupdate
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
@ -20,7 +20,7 @@ def update_spec(obj, spec):
return obj.__apispec__
def view2path(rule: str, view: Resource, spec: APISpec):
def view2path(rule: str, view: BaseResource, spec: APISpec):
params = {
"path": rule, # TODO: Validate this slightly (leading / etc)
"operations": view2operations(view, spec),
@ -35,9 +35,9 @@ def view2path(rule: str, view: Resource, spec: APISpec):
return params
def view2operations(view: Resource, spec: APISpec, populate_default: bool = True):
def view2operations(view: BaseResource, spec: APISpec, populate_default: bool = True):
ops = {}
for method in Resource.methods:
for method in BaseResource.methods:
if hasattr(view, method):
# Populate with default responses
if populate_default:

View file

@ -1,3 +1,10 @@
import collections.abc
import re
import base64
import operator
from functools import reduce
def get_docstring(obj):
ds = obj.__doc__
if ds:
@ -8,3 +15,49 @@ def get_docstring(obj):
def get_summary(obj):
return get_docstring(obj).partition("\n")[0].strip()
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
def get_by_path(root, items):
"""Access a nested object in root by item sequence."""
return reduce(operator.getitem, items, root)
def set_by_path(root, items, value):
"""Set a value in a nested object in root by item sequence."""
get_by_path(root, items[:-1])[items[-1]] = value
def create_from_path(items):
tree_dict = {}
for key in reversed(items):
tree_dict = {key: tree_dict}
return tree_dict
def bottom_level_name(obj):
return obj.__name__.split(".")[-1]
def camel_to_snake(name):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def camel_to_spine(name):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower()
def snake_to_spine(name):
return name.replace("_", "-")

View file

@ -12,9 +12,10 @@ from openflexure_microscope.camera.base import BaseCamera
from openflexure_microscope.camera.mock import MockStreamer
from openflexure_microscope.utilities import serialise_array_b64
from openflexure_microscope.common.labthings_core.lock import CompositeLock
from openflexure_microscope.config import user_settings
from openflexure_microscope.common.labthings_core.lock import CompositeLock
class Microscope:
"""

View file

@ -20,41 +20,6 @@ def serialise_array_b64(npy_arr):
return b64_string, dtype, shape
def get_by_path(root, items):
"""Access a nested object in root by item sequence."""
return reduce(operator.getitem, items, root)
def set_by_path(root, items, value):
"""Set a value in a nested object in root by item sequence."""
get_by_path(root, items[:-1])[items[-1]] = value
def create_from_path(items):
tree_dict = {}
for key in reversed(items):
tree_dict = {key: tree_dict}
return tree_dict
def bottom_level_name(obj):
return obj.__name__.split(".")[-1]
def camel_to_snake(name):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def camel_to_spine(name):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower()
def snake_to_spine(name):
return name.replace("_", "-")
@contextmanager
def set_properties(obj, **kwargs):
"""A context manager to set, then reset, certain properties of an object.