Added missing API documentation

This commit is contained in:
Joel Collins 2020-01-13 16:24:32 +00:00
parent 4003e4e65c
commit 9faa05aec1
8 changed files with 160 additions and 52 deletions

View file

@ -9,7 +9,7 @@ from openflexure_microscope.common.flask_labthings.resource import Resource
from openflexure_microscope.common.flask_labthings.utilities import (
description_from_view,
)
from openflexure_microscope.common.flask_labthings.decorators import marshal_with
from openflexure_microscope.common.flask_labthings.decorators import marshal_with, doc_response, Tag, ThingProperty
from openflexure_microscope.common.flask_labthings.find import find_component
@ -64,25 +64,26 @@ capture_schema = CaptureSchema()
capture_list_schema = CaptureSchema(many=True)
@ThingProperty
@Tag("captures")
class CaptureList(Resource):
"""
List all image captures
"""
@marshal_with(CaptureSchema(many=True))
def get(self):
"""
List all image captures
"""
microscope = find_component("org.openflexure.microscope")
image_list = microscope.camera.images
return image_list
@Tag("captures")
class CaptureResource(Resource):
"""
Description of a single image capture
"""
@marshal_with(CaptureSchema())
def get(self, id):
"""
Description of a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
@ -92,6 +93,9 @@ class CaptureResource(Resource):
return capture_obj
def delete(self, id):
"""
Delete a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
@ -103,13 +107,13 @@ class CaptureResource(Resource):
return "", 204
@Tag("captures")
class CaptureDownload(Resource):
"""
Image data for a single image capture
"""
@doc_response(200, mimetype="image/jpeg")
def get(self, id, filename):
"""
Image data for a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
@ -139,13 +143,12 @@ class CaptureDownload(Resource):
return send_file(img, mimetype="image/jpeg")
@Tag("captures")
class CaptureTags(Resource):
"""
Tags associated with a single image capture
"""
def get(self, id):
"""
Get tags associated with a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
@ -155,13 +158,16 @@ class CaptureTags(Resource):
return jsonify(capture_obj.tags)
def put(self, id):
"""
Add tags to a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
if not capture_obj:
return abort(404) # 404 Not Found
# TODO: Replace with normal Flask request JSON thing
data_dict = JsonResponse(request).json
if type(data_dict) != list:
@ -172,7 +178,9 @@ class CaptureTags(Resource):
return jsonify(capture_obj.tags)
def delete(self, capture_id):
"""
Delete tags from a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
@ -190,12 +198,12 @@ class CaptureTags(Resource):
return jsonify(capture_obj.tags)
@Tag("captures")
class CaptureMetadata(Resource):
"""
All metadata associated with a single image capture
"""
def get(self, id):
"""
Get metadata associated with a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
@ -205,6 +213,9 @@ class CaptureMetadata(Resource):
return jsonify(capture_obj.metadata)
def put(self, id):
"""
Update metadata for a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)

View file

@ -9,7 +9,7 @@ from openflexure_microscope.common.labthings_core.utilities import (
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.resource import Resource
from openflexure_microscope.common.flask_labthings.decorators import ThingProperty, marshal_with
from openflexure_microscope.common.flask_labthings.decorators import ThingProperty, Tag
from flask import jsonify, request, abort
import logging
@ -40,8 +40,12 @@ class SettingsProperty(Resource):
return self.get()
@Tag("properties")
class NestedSettingsProperty(Resource):
def get(self, route):
"""
Show a nested section of the current microscope settings
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
@ -53,6 +57,9 @@ class NestedSettingsProperty(Resource):
return jsonify(value)
def put(self, route):
"""
Update a nested section of the current microscope settings
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
payload = JsonResponse(request)
@ -76,8 +83,12 @@ class StatusProperty(Resource):
return jsonify(microscope.status)
@Tag("properties")
class NestedStatusProperty(Resource):
def get(self, route):
"""
Show a nested section of the current microscope state
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")

View file

@ -12,6 +12,7 @@ from openflexure_microscope.common.flask_labthings.decorators import doc_respons
from flask import Response
@ThingProperty
class MjpegStream(Resource):
"""

View file

@ -6,7 +6,7 @@ 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, get_spec
from .spec import rule2path, get_spec
from .views.extensions import ExtensionList
from .views.tasks import TaskList, TaskResource
@ -188,8 +188,8 @@ class LabThing(object):
return decorator
def _register_view(self, app, resource, *urls, endpoint=None, **kwargs):
endpoint = endpoint or resource.__name__.lower()
def _register_view(self, app, view, *urls, endpoint=None, **kwargs):
endpoint = endpoint or view.__name__.lower()
self.endpoints.add(endpoint)
resource_class_args = kwargs.pop("resource_class_args", ())
resource_class_kwargs = kwargs.pop("resource_class_kwargs", {})
@ -199,14 +199,14 @@ class LabThing(object):
previous_view_class = app.view_functions[endpoint].__dict__["view_class"]
# if you override the endpoint with a different class, avoid the collision by raising an exception
if previous_view_class != resource:
if previous_view_class != view:
raise ValueError(
"This endpoint (%s) is already set to the class %s."
% (endpoint, previous_view_class.__name__)
)
resource.endpoint = endpoint
resource_func = resource.as_view(
view.endpoint = endpoint
resource_func = view.as_view(
endpoint, *resource_class_args, **resource_class_kwargs
)
@ -216,15 +216,20 @@ class LabThing(object):
# 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))
#self.spec.path(**view2path(rule, view, self.spec))
# TEST: Getting Flask rule objects
flask_rules = app.url_map._rules_by_endpoint.get(endpoint)
for flask_rule in flask_rules:
self.spec.path(**rule2path(flask_rule, view, self.spec))
# Handle resource groups listed in API spec
view_spec = get_spec(resource)
view_spec = get_spec(view)
view_groups = view_spec.get("_groups", {})
if "actions" in view_groups:
self.actions[resource.endpoint] = resource
self.actions[view.endpoint] = view
if "properties" in view_groups:
self.properties[resource.endpoint] = resource
self.properties[view.endpoint] = view
### Utilities

View file

@ -1,4 +1,4 @@
from .resource import Resource
from ..resource import Resource
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
@ -8,9 +8,12 @@ from openflexure_microscope.common.labthings_core.utilities import (
rupdate,
)
from .fields import Field
from ..fields import Field
from marshmallow import Schema as BaseSchema
from .paths import rule_to_path, rule_to_params
from werkzeug.routing import Rule
from collections import Mapping
from http import HTTPStatus
@ -26,14 +29,22 @@ def get_spec(obj):
return obj.__apispec__
def view2path(rule: str, view: Resource, spec: APISpec):
def rule2path(rule: Rule, view: Resource, spec: APISpec):
params = {
"path": rule, # TODO: Validate this slightly (leading / etc)
"path": rule_to_path(rule),
"operations": view2operations(view, spec),
"description": get_docstring(view),
"summary": get_summary(view),
}
# Add URL arguments
if rule.arguments:
for op in params.get("operations").keys():
params["operations"][op].update({
"parameters": rule_to_params(rule)
})
# Add extra parameters
if hasattr(view, "__apispec__"):
# Recursively update params
rupdate(params, view.__apispec__)

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
import re
import werkzeug.routing
PATH_RE = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
def rule_to_path(rule):
return PATH_RE.sub(r'{\1}', rule.rule)
# Conversion map of werkzeug rule converters to Javascript schema types
CONVERTER_MAPPING = {
werkzeug.routing.UnicodeConverter: ('string', None),
werkzeug.routing.IntegerConverter: ('integer', 'int32'),
werkzeug.routing.FloatConverter: ('number', 'float'),
}
DEFAULT_TYPE = ('string', None)
def rule_to_params(rule, overrides=None):
overrides = (overrides or {})
result = [
argument_to_param(argument, rule, overrides.get(argument, {}))
for argument in rule.arguments
]
for key in overrides.keys():
if overrides[key].get('in') in ('header', 'query'):
overrides[key]['name'] = overrides[key].get('name', key)
result.append(overrides[key])
return result
def argument_to_param(argument, rule, override=None):
param = {
'in': 'path',
'name': argument,
'required': True,
}
type_, format_ = CONVERTER_MAPPING.get(type(rule._converters[argument]), DEFAULT_TYPE)
param['type'] = type_
if format_ is not None:
param['format'] = format_
if rule.defaults and argument in rule.defaults:
param['default'] = rule.defaults[argument]
param.update(override or {})
return param

View file

@ -1,9 +1,10 @@
from flask import abort, url_for, jsonify, render_template, Blueprint
from flask import abort, url_for, jsonify, render_template, Blueprint, current_app, request
from openflexure_microscope.common.labthings_core.utilities import get_docstring
from ...resource import Resource
from ...find import current_labthing
from ...spec import rule_to_path
import os
@ -35,8 +36,13 @@ class W3CThingDescriptionResource(Resource):
"""
def get(self):
base_url = request.host_url.rstrip('/')
props = {}
for key, prop in current_labthing().properties.items():
prop_rules = current_app.url_map._rules_by_endpoint.get(prop.endpoint)
prop_urls = [rule_to_path(rule) for rule in prop_rules]
props[key] = {}
props[key]["title"] = prop.__name__
# TODO: Get description from __apispec__ preferentially
@ -48,19 +54,22 @@ class W3CThingDescriptionResource(Resource):
)
props[key]["writeOnly"] = not hasattr(prop, "get")
props[key]["links"] = [
{"href": current_labthing().url_for(prop, _external=True)}
{"href": f"{base_url}{url}"} for url in prop_urls
]
actions = {}
for key, prop in current_labthing().actions.items():
for key, action in current_labthing().actions.items():
action_rules = current_app.url_map._rules_by_endpoint.get(action.endpoint)
action_urls = [rule_to_path(rule) for rule in action_rules]
actions[key] = {}
actions[key]["title"] = prop.__name__
actions[key]["title"] = action.__name__
# TODO: Get description from __apispec__ preferentially
actions[key]["description"] = get_docstring(prop) or (
get_docstring(prop.post) if hasattr(prop, "post") else ""
actions[key]["description"] = get_docstring(action) or (
get_docstring(action.post) if hasattr(action, "post") else ""
)
actions[key]["links"] = [
{"href": current_labthing().url_for(prop, _external=True)}
{"href": f"{base_url}{url}"} for url in action_urls
]
td = {

View file

@ -1,6 +1,6 @@
from flask import abort, url_for
from ..decorators import marshal_with, ThingProperty
from ..decorators import marshal_with, ThingProperty, Tag
from ..resource import Resource
from ..schema import TaskSchema
@ -9,18 +9,23 @@ from openflexure_microscope.common.labthings_core import tasks
@ThingProperty
class TaskList(Resource):
"""
List and basic documentation for all session tasks
"""
@marshal_with(TaskSchema(many=True))
def get(self):
"""
List of all session tasks
"""
return tasks.tasks()
@Tag("properties")
class TaskResource(Resource):
@marshal_with(TaskSchema())
def get(self, id):
"""
Show status of a session task
Includes progress and intermediate data.
"""
try:
task = tasks.dict()[id]
except KeyError:
@ -30,6 +35,11 @@ class TaskResource(Resource):
@marshal_with(TaskSchema())
def delete(self, id):
"""
Terminate a running task.
If the task is finished, deletes its entry.
"""
try:
task = tasks.dict()[id]
except KeyError: