From b7c9d3b73bd6ccb2c90cec207588f52045ae10ac Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 19 Jul 2021 20:09:09 +0100 Subject: [PATCH] Sorted remaining content_type issues and blackened --- docs/source/conf.py | 22 ++--- .../example_extension/05_actions.py | 2 +- docs/test_swagger_yaml.py | 1 + openflexure_microscope/api/app.py | 25 +++--- .../api/default_extensions/autofocus.py | 81 +++++++++++-------- openflexure_microscope/api/openapi.py | 26 ++---- .../api/v2/views/__init__.py | 2 +- .../api/v2/views/actions/camera.py | 4 +- openflexure_microscope/api/v2/views/camera.py | 2 +- .../api/v2/views/captures.py | 20 ++++- openflexure_microscope/api/v2/views/docs.py | 4 +- .../api/v2/views/streams.py | 11 +-- setup.py | 6 +- 13 files changed, 114 insertions(+), 92 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b1a2f862..23203631 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -44,7 +44,7 @@ project = "OpenFlexure Microscope Software" copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin author = "Bath Open Instrumentation Group" -#TODO: extract version from ../setup.py +# TODO: extract version from ../setup.py # The short X.Y version version = "" # The full version, including alpha/beta/rc tags @@ -68,11 +68,11 @@ extensions = [ "sphinx.ext.viewcode", "sphinx.ext.githubpages", "sphinx.ext.ifconfig", - #"sphinxcontrib.httpdomain", - #"sphinxcontrib.autohttp.flask", - #"sphinxcontrib.autohttp.flaskqref", + # "sphinxcontrib.httpdomain", + # "sphinxcontrib.autohttp.flask", + # "sphinxcontrib.autohttp.flaskqref", "sphinxcontrib.openapi", - "sphinxcontrib.redoc" + "sphinxcontrib.redoc", ] # Override ordering @@ -228,7 +228,7 @@ epub_exclude_files = ["search.html"] # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -#TODO: update with pysangaboard? +# TODO: update with pysangaboard? intersphinx_mapping = { "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), "picamerax": ("https://picamerax.readthedocs.io/en/latest//", None), @@ -244,9 +244,9 @@ todo_include_todos = True # -- Options for redoc extension --------------------------------------------- redoc = [ { - 'name': 'OpenFlexure Microscope HTTP API', - 'page': 'api_redoc', - 'spec': '../build/swagger.yaml', - 'embed': True, + "name": "OpenFlexure Microscope HTTP API", + "page": "api_redoc", + "spec": "../build/swagger.yaml", + "embed": True, } -] \ No newline at end of file +] diff --git a/docs/source/extensions/example_extension/05_actions.py b/docs/source/extensions/example_extension/05_actions.py index 0ac21880..ca2578db 100644 --- a/docs/source/extensions/example_extension/05_actions.py +++ b/docs/source/extensions/example_extension/05_actions.py @@ -94,7 +94,7 @@ class QuickCaptureAPI(ActionView): args = {"use_video_port": fields.Boolean(missing=True)} # Our success response (200) returns an image (image/jpeg mimetype) - responses = {200: {"content_type": "image/jpeg"}} + responses = {200: {"content": {"image/jpeg": {}}}} def post(self, args): """ diff --git a/docs/test_swagger_yaml.py b/docs/test_swagger_yaml.py index d37fcbd7..560a76bc 100644 --- a/docs/test_swagger_yaml.py +++ b/docs/test_swagger_yaml.py @@ -1,4 +1,5 @@ import yaml + with open("./build/swagger.yaml", "r") as f: api = yaml.load(f) diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 5384b828..ee56a86d 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -210,6 +210,7 @@ def routes(): def api_v1_catch_all(path): # pylint: disable=W0613 abort(410, "API v1 is no longer in use. Please upgrade your client.") + add_spec_extras(labthing.spec) @@ -247,36 +248,40 @@ def ofm_serve(): server: Server = Server(app) server.run(host="0.0.0.0", port=5000, debug=debug_app, zeroconf=True) + def generate_openapi(): parser = argparse.ArgumentParser("Generate an OpenAPI specification document") parser.add_argument( - '-o', - dest='output', + "-o", + dest="output", default="openapi.yaml", help=( - 'Specify the output filename. If it ends in .json, we output JSON.' - 'Use .yml or .yaml for YAML (which is the default' - ) + "Specify the output filename. If it ends in .json, we output JSON." + "Use .yml or .yaml for YAML (which is the default" + ), ) parser.add_argument( - "--validate", - action="store_true", - help="Validate the API spec, returning an error code if it does not pass." + "--validate", + action="store_true", + help="Validate the API spec, returning an error code if it does not pass.", ) args = parser.parse_args() if args.validate: import apispec.utils + if apispec.utils.validate_spec(labthing.spec): print("OpenAPI specification validated OK.") fname = args.output if fname.endswith(".json"): import json - with open(fname, 'w') as fd: + + with open(fname, "w") as fd: json.dump(labthing.spec.to_dict(), fd) else: - with open(fname, 'w') as fd: + with open(fname, "w") as fd: fd.write(labthing.spec.to_yaml()) + # Start the app if the module is run directly if __name__ == "__main__": ofm_serve() diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py index 3efd7973..90027d48 100644 --- a/openflexure_microscope/api/default_extensions/autofocus.py +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -24,6 +24,7 @@ class JPEGSharpnessMonitor: """Monitor JPEG frame size in a background thread This class starts a background thread """ + def __init__(self, microscope: Microscope): self.microscope: Microscope = microscope self.camera: BaseCamera = microscope.camera @@ -164,6 +165,7 @@ def find_microscope() -> Microscope: return microscope + def find_microscope_with_real_stage() -> Microscope: """Find the microscope and ensure it has a real stage. @@ -199,6 +201,7 @@ def extension_action(args=None): responsible for collating and adding the views in its `__init__` method. """ supplied_args = args + def decorator(func): class_docstring = f"""Manage actions for {func.__name__}. @@ -206,6 +209,7 @@ def extension_action(args=None): each time {func.__name__} has been run in response to a `GET` request, and will start a new `Action` in response to a `POST` request. """ + class ActionViewWrapper(ActionView): __doc__ = class_docstring args = supplied_args @@ -222,9 +226,9 @@ def extension_action(args=None): # aren't guaranteed to have the same leading whitespace, we just make sure # both docstrings are stripped of leading indent, using `inspect.cleandoc()` ActionViewWrapper.post.description = ( - get_docstring(func, remove_newlines=False) + - "\n\n" + - inspect.cleandoc( + get_docstring(func, remove_newlines=False) + + "\n\n" + + inspect.cleandoc( """ This `POST` request starts an Action, i.e. the hardware will do something that may continue after the HTTP request has been responded to. The @@ -242,11 +246,13 @@ def extension_action(args=None): ActionViewWrapper.post.summary = get_summary(func) ActionViewWrapper.post.__doc__ = ActionViewWrapper.post.description ActionViewWrapper.__name__ = func.__name__ - ActionViewWrapper.get.summary = f"List running and completed `{func.__name__}` actions." + ActionViewWrapper.get.summary = ( + f"List running and completed `{func.__name__}` actions." + ) ActionViewWrapper.get.description = ( - ActionViewWrapper.get.summary + - "\n\n" + - inspect.cleandoc( + ActionViewWrapper.get.summary + + "\n\n" + + inspect.cleandoc( f""" This `GET` request will return a list of `Action` objects corresponding to the `{func.__name__}` action. It will include all the times it has @@ -258,8 +264,8 @@ def extension_action(args=None): func.flask_view = ActionViewWrapper return func - return decorator + return decorator ### Autofocus extension @@ -291,7 +297,9 @@ class AutofocusExtension(BaseExtension): self.add_view(obj.flask_view, f"/{name}", name) def measure_sharpness( - self, microscope: Optional[Microscope] = None, metric_fn: Callable = sharpness_sum_lap2 + self, + microscope: Optional[Microscope] = None, + metric_fn: Callable = sharpness_sum_lap2, ) -> float: """Measure the sharpness from the MJPEG stream @@ -309,13 +317,13 @@ class AutofocusExtension(BaseExtension): raise RuntimeError(f"Object {microscope.camera} has no method `array`") @extension_action( - args = { + args={ "dz": fields.List( - fields.Int(), + fields.Int(), description="An ascending list of relative z positions", - example = [int(x) for x in np.linspace(-300, 300, 7)], + example=[int(x) for x in np.linspace(-300, 300, 7)], ) - }, + } ) def autofocus( self, @@ -360,16 +368,20 @@ class AutofocusExtension(BaseExtension): return positions, sharpnesses - def move_and_find_focus(self, microscope: Optional[Microscope] = None, dz: int = 0) -> int: + def move_and_find_focus( + self, microscope: Optional[Microscope] = None, dz: int = 0 + ) -> int: """Make a relative Z move and return the peak sharpness position""" if not microscope: microscope = find_microscope_with_real_stage() with monitor_sharpness(microscope) as m: m.focus_rel(dz) return m.sharpest_z_on_move(0) - + @extension_action( - args={"dz": fields.Int(required=True, description="The relative Z move to make")}, + args={ + "dz": fields.Int(required=True, description="The relative Z move to make") + } ) def move_and_measure( self, microscope: Optional[Microscope] = None, dz: int = 0 @@ -390,11 +402,11 @@ class AutofocusExtension(BaseExtension): @extension_action( args={ "dz": fields.Int( - missing=2000, - example=2000, - description="Total Z range to search over (in stage steps)" + missing=2000, + example=2000, + description="Total Z range to search over (in stage steps)", ) - }, + } ) def fast_autofocus( self, microscope: Optional[Microscope] = None, dz: int = 2000 @@ -484,27 +496,27 @@ class AutofocusExtension(BaseExtension): return m.data_dict() @extension_action( - args = { + args={ "dz": fields.Int( - missing = 2000, - example = 2000, - description = "Total Z range to search over (in stage steps)" + missing=2000, + example=2000, + description="Total Z range to search over (in stage steps)", ), "target_z": fields.Int( - missing = 0, - example = -100, - description = "Target finishing position, relative to the focus." + missing=0, + example=-100, + description="Target finishing position, relative to the focus.", ), "initial_move_up": fields.Bool( - missing = True, - description = "Set to Flase to disable the initial move upwards" + missing=True, + description="Set to Flase to disable the initial move upwards", ), "backlash": fields.Int( - missing=25, + missing=25, minimum=0, - description="Distance to undershoot, before correction move." + description="Distance to undershoot, before correction move.", ), - }, + } ) def fast_up_down_up_autofocus( self, @@ -563,7 +575,7 @@ class AutofocusExtension(BaseExtension): """ if not microscope: microscope = find_microscope_with_real_stage() - if not mini_backlash: # I renamed the argument, + if not mini_backlash: # I renamed the argument, mini_backlash = backlash or 25 with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock: with monitor_sharpness(microscope) as m: @@ -617,5 +629,6 @@ class AutofocusExtension(BaseExtension): class MeasureSharpnessAPI(View): __doc__ = AutofocusExtension.measure_sharpness.__doc__ + def post(self): - return {"sharpness": self.extension.measure_sharpness()} \ No newline at end of file + return {"sharpness": self.extension.measure_sharpness()} diff --git a/openflexure_microscope/api/openapi.py b/openflexure_microscope/api/openapi.py index 9996a57b..f0605954 100644 --- a/openflexure_microscope/api/openapi.py +++ b/openflexure_microscope/api/openapi.py @@ -1,5 +1,3 @@ - - API_TAGS = [ { "name": "actions", @@ -11,8 +9,8 @@ API_TAGS = [ ), "externalDocs": { "url": "https://iot.mozilla.org/wot/#action-resource", - "description": "Mozilla's description of Web of Things 'Action' resources." - } + "description": "Mozilla's description of Web of Things 'Action' resources.", + }, }, { "name": "properties", @@ -22,23 +20,15 @@ API_TAGS = [ ), "externalDocs": { "url": "https://iot.mozilla.org/wot/#property-resource", - "description": "Mozilla's description of Web of Things 'Property' resources." - } + "description": "Mozilla's description of Web of Things 'Property' resources.", + }, }, - { - "name": "captures", - "description": "", - }, - { - "name": "extensions", - "description": "", - }, - { - "name": "events", - "description": "", - } + {"name": "captures", "description": ""}, + {"name": "extensions", "description": ""}, + {"name": "events", "description": ""}, ] + def add_spec_extras(spec): """Add extra documentation and features to the OpenAPI spec""" # Add a list of tags, so we can control ordering and add descriptions diff --git a/openflexure_microscope/api/v2/views/__init__.py b/openflexure_microscope/api/v2/views/__init__.py index c5f59242..9cb10e41 100644 --- a/openflexure_microscope/api/v2/views/__init__.py +++ b/openflexure_microscope/api/v2/views/__init__.py @@ -4,4 +4,4 @@ from .captures import * from .instrument import * from .stage import * from .streams import * -from .docs import * \ No newline at end of file +from .docs import * diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py index 65b94eee..a6ef3b12 100644 --- a/openflexure_microscope/api/v2/views/actions/camera.py +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -71,8 +71,8 @@ class RAMCaptureAPI(ActionView): args = BasicCaptureArgs() responses = { 200: { - "content": { "image/jpeg": {} }, - "description": "A JPEG image, representing the capture" + "content": {"image/jpeg": {}}, + "description": "A JPEG image, representing the capture", } } diff --git a/openflexure_microscope/api/v2/views/camera.py b/openflexure_microscope/api/v2/views/camera.py index abe0c32e..9179ed10 100644 --- a/openflexure_microscope/api/v2/views/camera.py +++ b/openflexure_microscope/api/v2/views/camera.py @@ -14,7 +14,7 @@ class LSTImageProperty(PropertyView): responses = { 200: { - "content": { "image/jpeg": {} }, + "content": {"image/jpeg": {}}, "description": "Lens-shading table in RGB format", } } diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py index e3c1fce4..0903c35e 100644 --- a/openflexure_microscope/api/v2/views/captures.py +++ b/openflexure_microscope/api/v2/views/captures.py @@ -146,8 +146,19 @@ class CaptureList(PropertyView): return image_list +CAPTURE_ID_PARAMETER = { + "name": "id_", + "in": "path", + "description": "The unique ID of the capture", + "required": True, + "schema": {"type": "string"}, + "example": "eeae7ae9-0c0d-45a4-9ef2-7b84bb67a1d1", +} + + class CaptureView(View): tags = ["captures"] + parameters = [CAPTURE_ID_PARAMETER] @marshal_with(FullCaptureSchema()) def get(self, id_): @@ -162,6 +173,8 @@ class CaptureView(View): return capture_obj + get.responses = {404: {"description": "Capture object was not found"}} + def delete(self, id_): """ Delete a single image capture @@ -182,7 +195,10 @@ class CaptureView(View): class CaptureDownload(View): tags = ["captures"] - responses = {200: {"content_type": "image/jpeg"}} + responses = { + 200: {"content": {"image/jpeg": {}}, "description": "Image data in JPEG format"} + } + parameters = [CAPTURE_ID_PARAMETER] def get(self, id_, filename: Optional[str]): """ @@ -223,6 +239,7 @@ class CaptureDownload(View): class CaptureTags(View): tags = ["captures"] + parameters = [CAPTURE_ID_PARAMETER] def get(self, id_): """ @@ -270,6 +287,7 @@ class CaptureTags(View): class CaptureAnnotations(View): tags = ["captures"] + parameters = [CAPTURE_ID_PARAMETER] def get(self, id_): """ diff --git a/openflexure_microscope/api/v2/views/docs.py b/openflexure_microscope/api/v2/views/docs.py index 999668ac..d68c96f5 100644 --- a/openflexure_microscope/api/v2/views/docs.py +++ b/openflexure_microscope/api/v2/views/docs.py @@ -2,6 +2,7 @@ from labthings.views import View from labthings import current_labthing from flask import Response + class APISpecJSONView(View): """OpenAPI v3 documentation @@ -11,6 +12,7 @@ class APISpecJSONView(View): def get(self): return current_labthing().spec.to_dict() + class APISpecYAMLView(View): """OpenAPI v3 documentation @@ -18,4 +20,4 @@ class APISpecYAMLView(View): """ def get(self): - return Response(current_labthing().spec.to_yaml(), mimetype='text/yaml') \ No newline at end of file + return Response(current_labthing().spec.to_yaml(), mimetype="text/yaml") diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py index b9602103..4f5ba9ff 100644 --- a/openflexure_microscope/api/v2/views/streams.py +++ b/openflexure_microscope/api/v2/views/streams.py @@ -19,9 +19,7 @@ class MjpegStream(PropertyView): responses = { 200: { - "content": { - "multipart/x-mixed-replace": {} - }, + "content": {"multipart/x-mixed-replace": {}}, "description": ( "An MJPEG stream of camera images.\n\n" "This endpoint will serve JPEG images sequentially, \n" @@ -62,12 +60,7 @@ class SnapshotStream(PropertyView): Single JPEG snapshot from the camera stream """ - responses = { - 200: { - "content": { "image/jpeg": {} }, - "description": "Snapshot taken" - } - } + responses = {200: {"content": {"image/jpeg": {}}, "description": "Snapshot taken"}} def get(self): """ diff --git a/setup.py b/setup.py index 614961d5..061ec442 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ setup( # set up the project for development, you use those specific packages, rather than # the looser specifications given here. install_requires=[ - "apispec[validation]", # We need the extra to validate the spec + "apispec[validation]", # We need the extra to validate the spec "Flask ~= 1.0", "Pillow ~= 7.2.0", "numpy ~= 1.20", @@ -68,7 +68,7 @@ setup( # them to specific versions to enable consistent builds and testing. extras_require={ "dev": [ - "sphinx < 4.0", # Currently httpdomain isn't ready for 4.0 + "sphinx < 4.0", # Currently httpdomain isn't ready for 4.0 "sphinxcontrib-openapi ~= 0.7", "sphinx_rtd_theme ~=0.5.2", "rope ~= 0.14.0", @@ -90,7 +90,7 @@ setup( "console_scripts": [ "ofm-serve=openflexure_microscope.api.app:ofm_serve", "ofm-rescue=openflexure_microscope.rescue.auto:main", - "ofm-generate-openapi=openflexure_microscope.api.app:generate_openapi" + "ofm-generate-openapi=openflexure_microscope.api.app:generate_openapi", ] }, project_urls={