Added missing API documentation
This commit is contained in:
parent
4003e4e65c
commit
9faa05aec1
8 changed files with 160 additions and 52 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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("/")
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from openflexure_microscope.common.flask_labthings.decorators import doc_respons
|
|||
|
||||
from flask import Response
|
||||
|
||||
|
||||
@ThingProperty
|
||||
class MjpegStream(Resource):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
50
openflexure_microscope/common/flask_labthings/spec/paths.py
Normal file
50
openflexure_microscope/common/flask_labthings/spec/paths.py
Normal 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
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue