From a329b9197b01e6c960ca213081af5dcfccab350a Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 14 Jan 2020 11:41:56 +0000 Subject: [PATCH] Renamed Resource to View, and allow custom root links --- openflexure_microscope/api/app.py | 6 ++++- .../api/default_extensions/autofocus.py | 8 +++--- .../api/default_extensions/scan.py | 4 +-- .../api/default_extensions/zip_builder.py | 8 +++--- .../api/example_extensions/ev_gui.py | 6 ++--- .../api/v2/views/actions/camera.py | 8 +++--- .../api/v2/views/actions/stage.py | 6 ++--- .../api/v2/views/actions/system.py | 6 ++--- .../api/v2/views/captures.py | 16 ++++++------ openflexure_microscope/api/v2/views/state.py | 10 ++++---- .../api/v2/views/streams.py | 6 ++--- .../common/flask_labthings/labthing.py | 25 +++++++++++++------ .../common/flask_labthings/spec/__init__.py | 8 +++--- .../common/flask_labthings/utilities.py | 12 +++++++-- .../flask_labthings/{resource.py => view.py} | 4 +-- .../flask_labthings/views/docs/__init__.py | 14 +++++------ .../flask_labthings/views/extensions.py | 4 +-- .../common/flask_labthings/views/tasks.py | 6 ++--- 18 files changed, 90 insertions(+), 67 deletions(-) rename openflexure_microscope/common/flask_labthings/{resource.py => view.py} (91%) diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index f90d24c1..23f957b8 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -79,16 +79,20 @@ for extension in find_extensions(USER_EXTENSIONS_PATH): # Attach captures resources labthing.add_view(views.CaptureList, f"/captures") -labthing.add_view(views.CaptureResource, f"/captures/") +labthing.add_root_link(views.CaptureList, "captures") + +labthing.add_view(views.CaptureView, f"/captures/") labthing.add_view(views.CaptureDownload, f"/captures//download/") labthing.add_view(views.CaptureTags, f"/captures//tags") labthing.add_view(views.CaptureMetadata, f"/captures//metadata") # Attach settings and status resources labthing.add_view(views.SettingsProperty, f"/settings") +labthing.add_root_link(views.SettingsProperty, "settings") labthing.add_view(views.NestedSettingsProperty, "/settings/") labthing.add_view(views.StatusProperty, "/status") labthing.add_view(views.NestedStatusProperty, "/status/") +labthing.add_root_link(views.StatusProperty, "status") # Attach streams resources labthing.add_view(views.MjpegStream, f"/streams/mjpeg") diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py index 3c37caf0..2832835a 100644 --- a/openflexure_microscope/api/default_extensions/autofocus.py +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -1,6 +1,6 @@ from openflexure_microscope.common.flask_labthings.find import find_component from openflexure_microscope.common.flask_labthings.extensions import BaseExtension -from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.view import View from openflexure_microscope.common.flask_labthings.decorators import ( ThingAction, ThingProperty, @@ -292,7 +292,7 @@ def fast_up_down_up_autofocus( return m.data_dict() -class MeasureSharpnessAPI(Resource): +class MeasureSharpnessAPI(View): def post(self): microscope = find_component("org.openflexure.microscope") @@ -303,7 +303,7 @@ class MeasureSharpnessAPI(Resource): @ThingAction -class AutofocusAPI(Resource): +class AutofocusAPI(View): """ Run a standard autofocus """ @@ -330,7 +330,7 @@ class AutofocusAPI(Resource): @ThingAction -class FastAutofocusAPI(Resource): +class FastAutofocusAPI(View): """ Run a fast autofocus """ diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py index 5dd87446..3b25e2e0 100644 --- a/openflexure_microscope/api/default_extensions/scan.py +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -19,7 +19,7 @@ from openflexure_microscope.common.flask_labthings import fields from openflexure_microscope.devel import taskify, abort, update_task_progress -from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.view import View import time @@ -341,7 +341,7 @@ def stack( @ThingAction -class TileScanAPI(Resource): +class TileScanAPI(View): @use_args( { "filename": fields.String(), diff --git a/openflexure_microscope/api/default_extensions/zip_builder.py b/openflexure_microscope/api/default_extensions/zip_builder.py index 9b1d8757..8558036f 100644 --- a/openflexure_microscope/api/default_extensions/zip_builder.py +++ b/openflexure_microscope/api/default_extensions/zip_builder.py @@ -15,7 +15,7 @@ import tempfile import logging 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.view import View from openflexure_microscope.common.flask_labthings.extensions import BaseExtension from openflexure_microscope.common.flask_labthings.decorators import ( ThingAction, @@ -96,7 +96,7 @@ default_zip_manager = ZipManager() @ThingAction -class ZipBuilderAPIView(Resource): +class ZipBuilderAPIView(View): def post(self): ids = list(JsonResponse(request).json) @@ -109,12 +109,12 @@ class ZipBuilderAPIView(Resource): @ThingProperty -class ZipListAPIView(Resource): +class ZipListAPIView(View): def get(self): return jsonify(default_zip_manager.session_zips) -class ZipGetterAPIView(Resource): +class ZipGetterAPIView(View): def get(self, session_id): if not session_id in default_zip_manager.session_zips: return abort(404) # 404 Not Found diff --git a/openflexure_microscope/api/example_extensions/ev_gui.py b/openflexure_microscope/api/example_extensions/ev_gui.py index da6f9efc..469c02aa 100644 --- a/openflexure_microscope/api/example_extensions/ev_gui.py +++ b/openflexure_microscope/api/example_extensions/ev_gui.py @@ -1,4 +1,4 @@ -from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.view import View from openflexure_microscope.common.flask_labthings.extensions import BaseExtension from openflexure_microscope.common.flask_labthings.decorators import ( ThingAction, @@ -77,7 +77,7 @@ static_form = { @ThingProperty -class TestAPIView(Resource): +class TestAPIView(View): def get(self): global val_int val_int += 1 @@ -85,7 +85,7 @@ class TestAPIView(Resource): @ThingAction -class TestDoAPIView(Resource): +class TestDoAPIView(View): def post(self): global val_int val_int += 1 diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py index ef507fe5..917a72eb 100644 --- a/openflexure_microscope/api/v2/views/actions/camera.py +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -1,5 +1,5 @@ from openflexure_microscope.api.utilities import get_bool, JsonResponse -from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.view import View from openflexure_microscope.common.flask_labthings.find import find_component from openflexure_microscope.common.flask_labthings.decorators import ( use_args, @@ -20,7 +20,7 @@ from flask import jsonify, request, abort, url_for, redirect, send_file @ThingAction -class CaptureAPI(Resource): +class CaptureAPI(View): """ Create a new image capture. """ @@ -86,7 +86,7 @@ class CaptureAPI(Resource): @ThingAction -class GPUPreviewStartAPI(Resource): +class GPUPreviewStartAPI(View): """ Start the onboard GPU preview. Optional "window" parameter can be passed to control the position and size of the preview window, @@ -119,7 +119,7 @@ class GPUPreviewStartAPI(Resource): @ThingAction -class GPUPreviewStopAPI(Resource): +class GPUPreviewStopAPI(View): def post(self): """ Stop the onboard GPU preview. diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py index c1fec897..35795781 100644 --- a/openflexure_microscope/api/v2/views/actions/stage.py +++ b/openflexure_microscope/api/v2/views/actions/stage.py @@ -1,5 +1,5 @@ from openflexure_microscope.api.utilities import JsonResponse -from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.view import View from openflexure_microscope.common.flask_labthings.find import find_component from openflexure_microscope.common.flask_labthings.decorators import ( use_args, @@ -17,7 +17,7 @@ import logging @ThingAction -class MoveStageAPI(Resource): +class MoveStageAPI(View): @use_args( { "absolute": fields.Boolean( @@ -62,7 +62,7 @@ class MoveStageAPI(Resource): @ThingAction -class ZeroStageAPI(Resource): +class ZeroStageAPI(View): def post(self): """ Zero the stage coordinates. diff --git a/openflexure_microscope/api/v2/views/actions/system.py b/openflexure_microscope/api/v2/views/actions/system.py index 82b67b92..a370d05e 100644 --- a/openflexure_microscope/api/v2/views/actions/system.py +++ b/openflexure_microscope/api/v2/views/actions/system.py @@ -1,4 +1,4 @@ -from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.view import View import subprocess import os from sys import platform @@ -18,7 +18,7 @@ def is_raspberrypi(raise_on_errors=False): @ThingAction -class ShutdownAPI(Resource): +class ShutdownAPI(View): """ Attempt to shutdown the device """ @@ -34,7 +34,7 @@ class ShutdownAPI(Resource): @ThingAction -class RebootAPI(Resource): +class RebootAPI(View): """ Attempt to reboot the device """ diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py index 74bf97e6..31eef23c 100644 --- a/openflexure_microscope/api/v2/views/captures.py +++ b/openflexure_microscope/api/v2/views/captures.py @@ -5,7 +5,7 @@ from openflexure_microscope.api.utilities import get_bool, JsonResponse from openflexure_microscope.common.flask_labthings.schema import Schema from openflexure_microscope.common.flask_labthings import fields -from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.view import View from openflexure_microscope.common.flask_labthings.utilities import ( description_from_view, ) @@ -32,9 +32,9 @@ class CaptureSchema(Schema): def generate_links(self, data, **kwargs): data.links = { "self": { - "href": url_for(CaptureResource.endpoint, id=data.id, _external=True), + "href": url_for(CaptureView.endpoint, id=data.id, _external=True), "mimetype": "application/json", - **description_from_view(CaptureResource), + **description_from_view(CaptureView), }, "tags": { "href": url_for(CaptureTags.endpoint, id=data.id, _external=True), @@ -66,7 +66,7 @@ capture_list_schema = CaptureSchema(many=True) @ThingProperty @Tag("captures") -class CaptureList(Resource): +class CaptureList(View): @marshal_with(CaptureSchema(many=True)) def get(self): """ @@ -78,7 +78,7 @@ class CaptureList(Resource): @Tag("captures") -class CaptureResource(Resource): +class CaptureView(View): @marshal_with(CaptureSchema()) def get(self, id): """ @@ -108,7 +108,7 @@ class CaptureResource(Resource): @Tag("captures") -class CaptureDownload(Resource): +class CaptureDownload(View): @doc_response(200, mimetype="image/jpeg") def get(self, id, filename): """ @@ -144,7 +144,7 @@ class CaptureDownload(Resource): @Tag("captures") -class CaptureTags(Resource): +class CaptureTags(View): def get(self, id): """ Get tags associated with a single image capture @@ -199,7 +199,7 @@ class CaptureTags(Resource): @Tag("captures") -class CaptureMetadata(Resource): +class CaptureMetadata(View): def get(self, id): """ Get metadata associated with a single image capture diff --git a/openflexure_microscope/api/v2/views/state.py b/openflexure_microscope/api/v2/views/state.py index 1da71698..b46b4e63 100644 --- a/openflexure_microscope/api/v2/views/state.py +++ b/openflexure_microscope/api/v2/views/state.py @@ -7,7 +7,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.view import View from openflexure_microscope.common.flask_labthings.decorators import ThingProperty, Tag, doc_response @@ -16,7 +16,7 @@ import logging @ThingProperty -class SettingsProperty(Resource): +class SettingsProperty(View): def get(self): """ Current microscope settings, including camera and stage @@ -41,7 +41,7 @@ class SettingsProperty(Resource): @Tag("properties") -class NestedSettingsProperty(Resource): +class NestedSettingsProperty(View): @doc_response(404, description="Settings key cannot be found") def get(self, route): """ @@ -76,7 +76,7 @@ class NestedSettingsProperty(Resource): @ThingProperty -class StatusProperty(Resource): +class StatusProperty(View): def get(self): """ Show current read-only state of the microscope @@ -86,7 +86,7 @@ class StatusProperty(Resource): @Tag("properties") -class NestedStatusProperty(Resource): +class NestedStatusProperty(View): @doc_response(404, description="Status key cannot be found") def get(self, route): """ diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py index 967c6da7..8234d52f 100644 --- a/openflexure_microscope/api/v2/views/streams.py +++ b/openflexure_microscope/api/v2/views/streams.py @@ -7,14 +7,14 @@ 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.view import View from openflexure_microscope.common.flask_labthings.decorators import doc_response, ThingProperty from flask import Response @ThingProperty -class MjpegStream(Resource): +class MjpegStream(View): """ Real-time MJPEG stream from the microscope camera """ @@ -34,7 +34,7 @@ class MjpegStream(Resource): @ThingProperty -class SnapshotStream(Resource): +class SnapshotStream(View): """ Single JPEG snapshot from the camera stream """ diff --git a/openflexure_microscope/common/flask_labthings/labthing.py b/openflexure_microscope/common/flask_labthings/labthing.py index 8c3fecfd..ce1e17f3 100644 --- a/openflexure_microscope/common/flask_labthings/labthing.py +++ b/openflexure_microscope/common/flask_labthings/labthing.py @@ -10,8 +10,8 @@ from .spec import rule2path, get_spec from .decorators import tag from .views.extensions import ExtensionList -from .views.tasks import TaskList, TaskResource -from .views.docs import docs_blueprint, SwaggerUIResource, W3CThingDescriptionResource +from .views.tasks import TaskList, TaskView +from .views.docs import docs_blueprint, SwaggerUIView, W3CThingDescriptionView from openflexure_microscope.common.labthings_core.utilities import get_docstring @@ -37,6 +37,8 @@ class LabThing(object): self.properties = {} self.actions = {} + self.custom_root_links = {} + self.endpoints = set() self.url_prefix = prefix @@ -114,7 +116,7 @@ class LabThing(object): self.add_view(ExtensionList, "/extensions", endpoint=EXTENSION_LIST_ENDPOINT) # Add task routes self.add_view(TaskList, "/tasks", endpoint=TASK_LIST_ENDPOINT) - self.add_view(TaskResource, "/tasks/", endpoint=TASK_ENDPOINT) + self.add_view(TaskView, "/tasks/", endpoint=TASK_ENDPOINT) ### Device stuff @@ -234,15 +236,18 @@ class LabThing(object): ### Utilities - def url_for(self, resource, **values): + def url_for(self, view, **values): """Generates a URL to the given resource. Works like :func:`flask.url_for`.""" - endpoint = resource.endpoint + endpoint = view.endpoint return url_for(endpoint, **values) def owns_endpoint(self, endpoint): return endpoint in self.endpoints + def add_root_link(self, view, title, kwargs={}): + self.custom_root_links[title] = (view, kwargs) + ### Description def rootrep(self): """ @@ -257,11 +262,11 @@ class LabThing(object): "links": { "thingDescription": { "href": url_for("labthings_docs.w3c_td", _external=True), - "description": get_docstring(W3CThingDescriptionResource), + "description": get_docstring(W3CThingDescriptionView), }, "swaggerUI": { "href": url_for("labthings_docs.swagger_ui", _external=True), - **description_from_view(SwaggerUIResource), + **description_from_view(SwaggerUIView), }, "extensions": { "href": self.url_for(ExtensionList, _external=True), @@ -274,4 +279,10 @@ class LabThing(object): }, } + for title, (view, kwargs) in self.custom_root_links.items(): + rr["links"][title] = { + "href": self.url_for(view, **kwargs, _external=True), + **description_from_view(view), + } + return jsonify(rr) diff --git a/openflexure_microscope/common/flask_labthings/spec/__init__.py b/openflexure_microscope/common/flask_labthings/spec/__init__.py index 59a1bae7..eb5bef96 100644 --- a/openflexure_microscope/common/flask_labthings/spec/__init__.py +++ b/openflexure_microscope/common/flask_labthings/spec/__init__.py @@ -1,4 +1,4 @@ -from ..resource import Resource +from ..view import View from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin @@ -29,7 +29,7 @@ def get_spec(obj): return obj.__apispec__ -def rule2path(rule: Rule, view: Resource, spec: APISpec): +def rule2path(rule: Rule, view: View, spec: APISpec): params = { "path": rule_to_path(rule), "operations": view2operations(view, spec), @@ -52,7 +52,7 @@ def rule2path(rule: Rule, view: Resource, spec: APISpec): return params -def view2operations(view: Resource, spec: APISpec): +def view2operations(view: View, spec: APISpec): # Operations inherit tags from parent inherited_tags = [] if hasattr(view, "__apispec__"): @@ -60,7 +60,7 @@ def view2operations(view: Resource, spec: APISpec): # Build dictionary of operations (HTTP methods) ops = {} - for method in Resource.methods: + for method in View.methods: if hasattr(view, method): ops[method] = {} diff --git a/openflexure_microscope/common/flask_labthings/utilities.py b/openflexure_microscope/common/flask_labthings/utilities.py index e06dad4c..86c904da 100644 --- a/openflexure_microscope/common/flask_labthings/utilities.py +++ b/openflexure_microscope/common/flask_labthings/utilities.py @@ -2,15 +2,23 @@ from openflexure_microscope.common.labthings_core.utilities import ( get_docstring, get_summary, ) + +from .view import View + from flask import current_app def description_from_view(view_class): + summary = get_summary(view_class) + methods = [] - for method_key in ["get", "post", "put", "delete"]: + for method_key in View.methods: if hasattr(view_class, method_key): methods.append(method_key.upper()) - summary = get_summary(view_class) + + # If no class summary was given, try using summaries from method functions + if not summary: + summary = get_summary(getattr(view_class, method_key)) d = {"methods": methods, "description": summary} diff --git a/openflexure_microscope/common/flask_labthings/resource.py b/openflexure_microscope/common/flask_labthings/view.py similarity index 91% rename from openflexure_microscope/common/flask_labthings/resource.py rename to openflexure_microscope/common/flask_labthings/view.py index ed3ef119..0998a199 100644 --- a/openflexure_microscope/common/flask_labthings/resource.py +++ b/openflexure_microscope/common/flask_labthings/view.py @@ -1,7 +1,7 @@ from flask.views import MethodView -class Resource(MethodView): +class View(MethodView): """ A LabThing Resource class should make use of functions get(), put(), post(), and delete() corresponding to HTTP methods. @@ -20,7 +20,7 @@ class Resource(MethodView): if hasattr(self, "__apispec__"): docs.update(self.__apispec__) - for meth in BaseResource.methods: + for meth in View.methods: if hasattr(self, meth) and hasattr(getattr(self, meth), "__apispec__"): docs["operations"][meth] = {} docs["operations"][meth] = getattr(self, meth).__apispec__ diff --git a/openflexure_microscope/common/flask_labthings/views/docs/__init__.py b/openflexure_microscope/common/flask_labthings/views/docs/__init__.py index 2adad8ab..4da62d29 100644 --- a/openflexure_microscope/common/flask_labthings/views/docs/__init__.py +++ b/openflexure_microscope/common/flask_labthings/views/docs/__init__.py @@ -2,14 +2,14 @@ from flask import abort, url_for, jsonify, render_template, Blueprint, current_a from openflexure_microscope.common.labthings_core.utilities import get_docstring -from ...resource import Resource +from ...view import View from ...find import current_labthing from ...spec import rule_to_path, rule_to_params import os -class APISpecResource(Resource): +class APISpecView(View): """ OpenAPI v3 documentation """ @@ -21,7 +21,7 @@ class APISpecResource(Resource): return jsonify(current_labthing().spec.to_dict()) -class SwaggerUIResource(Resource): +class SwaggerUIView(View): """ Swagger UI documentation """ @@ -30,7 +30,7 @@ class SwaggerUIResource(Resource): return render_template("swagger-ui.html") -class W3CThingDescriptionResource(Resource): +class W3CThingDescriptionView(View): """ W3C-style Thing Description """ @@ -103,11 +103,11 @@ docs_blueprint = Blueprint( ) docs_blueprint.add_url_rule( - "/swagger", view_func=APISpecResource.as_view("swagger_json") + "/swagger", view_func=APISpecView.as_view("swagger_json") ) docs_blueprint.add_url_rule( - "/swagger-ui", view_func=SwaggerUIResource.as_view("swagger_ui") + "/swagger-ui", view_func=SwaggerUIView.as_view("swagger_ui") ) docs_blueprint.add_url_rule( - "/td", view_func=W3CThingDescriptionResource.as_view("w3c_td") + "/td", view_func=W3CThingDescriptionView.as_view("w3c_td") ) diff --git a/openflexure_microscope/common/flask_labthings/views/extensions.py b/openflexure_microscope/common/flask_labthings/views/extensions.py index 6e1c01c8..66684682 100644 --- a/openflexure_microscope/common/flask_labthings/views/extensions.py +++ b/openflexure_microscope/common/flask_labthings/views/extensions.py @@ -1,14 +1,14 @@ """ Top-level representation of attached and enabled Extensions """ -from ..resource import Resource +from ..view import View from ..find import registered_extensions from ..schema import ExtensionSchema from ..decorators import marshal_with, ThingProperty @ThingProperty -class ExtensionList(Resource): +class ExtensionList(View): """ List and basic documentation for all enabled Extensions """ diff --git a/openflexure_microscope/common/flask_labthings/views/tasks.py b/openflexure_microscope/common/flask_labthings/views/tasks.py index 06a136a7..f583ba17 100644 --- a/openflexure_microscope/common/flask_labthings/views/tasks.py +++ b/openflexure_microscope/common/flask_labthings/views/tasks.py @@ -1,7 +1,7 @@ from flask import abort, url_for from ..decorators import marshal_with, ThingProperty, Tag -from ..resource import Resource +from ..view import View from ..schema import TaskSchema from openflexure_microscope.common.labthings_core import tasks @@ -9,7 +9,7 @@ from openflexure_microscope.common.labthings_core import tasks @ThingProperty @Tag("tasks") -class TaskList(Resource): +class TaskList(View): @marshal_with(TaskSchema(many=True)) def get(self): """ @@ -19,7 +19,7 @@ class TaskList(Resource): @Tag(["properties", "tasks"]) -class TaskResource(Resource): +class TaskView(View): @marshal_with(TaskSchema()) def get(self, id): """