Sorted remaining content_type issues and blackened

This commit is contained in:
Richard 2021-07-19 20:09:09 +01:00
parent 38fbe530d2
commit b7c9d3b73b
13 changed files with 114 additions and 92 deletions

View file

@ -44,7 +44,7 @@ project = "OpenFlexure Microscope Software"
copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin
author = "Bath Open Instrumentation Group" author = "Bath Open Instrumentation Group"
#TODO: extract version from ../setup.py # TODO: extract version from ../setup.py
# The short X.Y version # The short X.Y version
version = "" version = ""
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
@ -68,11 +68,11 @@ extensions = [
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinx.ext.githubpages", "sphinx.ext.githubpages",
"sphinx.ext.ifconfig", "sphinx.ext.ifconfig",
#"sphinxcontrib.httpdomain", # "sphinxcontrib.httpdomain",
#"sphinxcontrib.autohttp.flask", # "sphinxcontrib.autohttp.flask",
#"sphinxcontrib.autohttp.flaskqref", # "sphinxcontrib.autohttp.flaskqref",
"sphinxcontrib.openapi", "sphinxcontrib.openapi",
"sphinxcontrib.redoc" "sphinxcontrib.redoc",
] ]
# Override ordering # Override ordering
@ -228,7 +228,7 @@ epub_exclude_files = ["search.html"]
# -- Options for intersphinx extension --------------------------------------- # -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
#TODO: update with pysangaboard? # TODO: update with pysangaboard?
intersphinx_mapping = { intersphinx_mapping = {
"openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None),
"picamerax": ("https://picamerax.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 --------------------------------------------- # -- Options for redoc extension ---------------------------------------------
redoc = [ redoc = [
{ {
'name': 'OpenFlexure Microscope HTTP API', "name": "OpenFlexure Microscope HTTP API",
'page': 'api_redoc', "page": "api_redoc",
'spec': '../build/swagger.yaml', "spec": "../build/swagger.yaml",
'embed': True, "embed": True,
} }
] ]

View file

@ -94,7 +94,7 @@ class QuickCaptureAPI(ActionView):
args = {"use_video_port": fields.Boolean(missing=True)} args = {"use_video_port": fields.Boolean(missing=True)}
# Our success response (200) returns an image (image/jpeg mimetype) # 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): def post(self, args):
""" """

View file

@ -1,4 +1,5 @@
import yaml import yaml
with open("./build/swagger.yaml", "r") as f: with open("./build/swagger.yaml", "r") as f:
api = yaml.load(f) api = yaml.load(f)

View file

@ -210,6 +210,7 @@ def routes():
def api_v1_catch_all(path): # pylint: disable=W0613 def api_v1_catch_all(path): # pylint: disable=W0613
abort(410, "API v1 is no longer in use. Please upgrade your client.") abort(410, "API v1 is no longer in use. Please upgrade your client.")
add_spec_extras(labthing.spec) add_spec_extras(labthing.spec)
@ -247,36 +248,40 @@ def ofm_serve():
server: Server = Server(app) server: Server = Server(app)
server.run(host="0.0.0.0", port=5000, debug=debug_app, zeroconf=True) server.run(host="0.0.0.0", port=5000, debug=debug_app, zeroconf=True)
def generate_openapi(): def generate_openapi():
parser = argparse.ArgumentParser("Generate an OpenAPI specification document") parser = argparse.ArgumentParser("Generate an OpenAPI specification document")
parser.add_argument( parser.add_argument(
'-o', "-o",
dest='output', dest="output",
default="openapi.yaml", default="openapi.yaml",
help=( help=(
'Specify the output filename. If it ends in .json, we output JSON.' "Specify the output filename. If it ends in .json, we output JSON."
'Use .yml or .yaml for YAML (which is the default' "Use .yml or .yaml for YAML (which is the default"
) ),
) )
parser.add_argument( parser.add_argument(
"--validate", "--validate",
action="store_true", action="store_true",
help="Validate the API spec, returning an error code if it does not pass." help="Validate the API spec, returning an error code if it does not pass.",
) )
args = parser.parse_args() args = parser.parse_args()
if args.validate: if args.validate:
import apispec.utils import apispec.utils
if apispec.utils.validate_spec(labthing.spec): if apispec.utils.validate_spec(labthing.spec):
print("OpenAPI specification validated OK.") print("OpenAPI specification validated OK.")
fname = args.output fname = args.output
if fname.endswith(".json"): if fname.endswith(".json"):
import json import json
with open(fname, 'w') as fd:
with open(fname, "w") as fd:
json.dump(labthing.spec.to_dict(), fd) json.dump(labthing.spec.to_dict(), fd)
else: else:
with open(fname, 'w') as fd: with open(fname, "w") as fd:
fd.write(labthing.spec.to_yaml()) fd.write(labthing.spec.to_yaml())
# Start the app if the module is run directly # Start the app if the module is run directly
if __name__ == "__main__": if __name__ == "__main__":
ofm_serve() ofm_serve()

View file

@ -24,6 +24,7 @@ class JPEGSharpnessMonitor:
"""Monitor JPEG frame size in a background thread """Monitor JPEG frame size in a background thread
This class starts a background thread """ This class starts a background thread """
def __init__(self, microscope: Microscope): def __init__(self, microscope: Microscope):
self.microscope: Microscope = microscope self.microscope: Microscope = microscope
self.camera: BaseCamera = microscope.camera self.camera: BaseCamera = microscope.camera
@ -164,6 +165,7 @@ def find_microscope() -> Microscope:
return microscope return microscope
def find_microscope_with_real_stage() -> Microscope: def find_microscope_with_real_stage() -> Microscope:
"""Find the microscope and ensure it has a real stage. """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. responsible for collating and adding the views in its `__init__` method.
""" """
supplied_args = args supplied_args = args
def decorator(func): def decorator(func):
class_docstring = f"""Manage actions for {func.__name__}. 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, 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. and will start a new `Action` in response to a `POST` request.
""" """
class ActionViewWrapper(ActionView): class ActionViewWrapper(ActionView):
__doc__ = class_docstring __doc__ = class_docstring
args = supplied_args 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 # aren't guaranteed to have the same leading whitespace, we just make sure
# both docstrings are stripped of leading indent, using `inspect.cleandoc()` # both docstrings are stripped of leading indent, using `inspect.cleandoc()`
ActionViewWrapper.post.description = ( ActionViewWrapper.post.description = (
get_docstring(func, remove_newlines=False) + get_docstring(func, remove_newlines=False)
"\n\n" + + "\n\n"
inspect.cleandoc( + inspect.cleandoc(
""" """
This `POST` request starts an Action, i.e. the hardware will do something 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 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.summary = get_summary(func)
ActionViewWrapper.post.__doc__ = ActionViewWrapper.post.description ActionViewWrapper.post.__doc__ = ActionViewWrapper.post.description
ActionViewWrapper.__name__ = func.__name__ 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.description = (
ActionViewWrapper.get.summary + ActionViewWrapper.get.summary
"\n\n" + + "\n\n"
inspect.cleandoc( + inspect.cleandoc(
f""" f"""
This `GET` request will return a list of `Action` objects corresponding This `GET` request will return a list of `Action` objects corresponding
to the `{func.__name__}` action. It will include all the times it has 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 func.flask_view = ActionViewWrapper
return func return func
return decorator
return decorator
### Autofocus extension ### Autofocus extension
@ -291,7 +297,9 @@ class AutofocusExtension(BaseExtension):
self.add_view(obj.flask_view, f"/{name}", name) self.add_view(obj.flask_view, f"/{name}", name)
def measure_sharpness( 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: ) -> float:
"""Measure the sharpness from the MJPEG stream """Measure the sharpness from the MJPEG stream
@ -309,13 +317,13 @@ class AutofocusExtension(BaseExtension):
raise RuntimeError(f"Object {microscope.camera} has no method `array`") raise RuntimeError(f"Object {microscope.camera} has no method `array`")
@extension_action( @extension_action(
args = { args={
"dz": fields.List( "dz": fields.List(
fields.Int(), fields.Int(),
description="An ascending list of relative z positions", 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( def autofocus(
self, self,
@ -360,16 +368,20 @@ class AutofocusExtension(BaseExtension):
return positions, sharpnesses 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""" """Make a relative Z move and return the peak sharpness position"""
if not microscope: if not microscope:
microscope = find_microscope_with_real_stage() microscope = find_microscope_with_real_stage()
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m:
m.focus_rel(dz) m.focus_rel(dz)
return m.sharpest_z_on_move(0) return m.sharpest_z_on_move(0)
@extension_action( @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( def move_and_measure(
self, microscope: Optional[Microscope] = None, dz: int = 0 self, microscope: Optional[Microscope] = None, dz: int = 0
@ -390,11 +402,11 @@ class AutofocusExtension(BaseExtension):
@extension_action( @extension_action(
args={ args={
"dz": fields.Int( "dz": fields.Int(
missing=2000, missing=2000,
example=2000, example=2000,
description="Total Z range to search over (in stage steps)" description="Total Z range to search over (in stage steps)",
) )
}, }
) )
def fast_autofocus( def fast_autofocus(
self, microscope: Optional[Microscope] = None, dz: int = 2000 self, microscope: Optional[Microscope] = None, dz: int = 2000
@ -484,27 +496,27 @@ class AutofocusExtension(BaseExtension):
return m.data_dict() return m.data_dict()
@extension_action( @extension_action(
args = { args={
"dz": fields.Int( "dz": fields.Int(
missing = 2000, missing=2000,
example = 2000, example=2000,
description = "Total Z range to search over (in stage steps)" description="Total Z range to search over (in stage steps)",
), ),
"target_z": fields.Int( "target_z": fields.Int(
missing = 0, missing=0,
example = -100, example=-100,
description = "Target finishing position, relative to the focus." description="Target finishing position, relative to the focus.",
), ),
"initial_move_up": fields.Bool( "initial_move_up": fields.Bool(
missing = True, missing=True,
description = "Set to Flase to disable the initial move upwards" description="Set to Flase to disable the initial move upwards",
), ),
"backlash": fields.Int( "backlash": fields.Int(
missing=25, missing=25,
minimum=0, minimum=0,
description="Distance to undershoot, before correction move." description="Distance to undershoot, before correction move.",
), ),
}, }
) )
def fast_up_down_up_autofocus( def fast_up_down_up_autofocus(
self, self,
@ -563,7 +575,7 @@ class AutofocusExtension(BaseExtension):
""" """
if not microscope: if not microscope:
microscope = find_microscope_with_real_stage() 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 mini_backlash = backlash or 25
with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock: with microscope.lock(timeout=1), microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m:
@ -617,5 +629,6 @@ class AutofocusExtension(BaseExtension):
class MeasureSharpnessAPI(View): class MeasureSharpnessAPI(View):
__doc__ = AutofocusExtension.measure_sharpness.__doc__ __doc__ = AutofocusExtension.measure_sharpness.__doc__
def post(self): def post(self):
return {"sharpness": self.extension.measure_sharpness()} return {"sharpness": self.extension.measure_sharpness()}

View file

@ -1,5 +1,3 @@
API_TAGS = [ API_TAGS = [
{ {
"name": "actions", "name": "actions",
@ -11,8 +9,8 @@ API_TAGS = [
), ),
"externalDocs": { "externalDocs": {
"url": "https://iot.mozilla.org/wot/#action-resource", "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", "name": "properties",
@ -22,23 +20,15 @@ API_TAGS = [
), ),
"externalDocs": { "externalDocs": {
"url": "https://iot.mozilla.org/wot/#property-resource", "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": "captures", {"name": "extensions", "description": ""},
"description": "", {"name": "events", "description": ""},
},
{
"name": "extensions",
"description": "",
},
{
"name": "events",
"description": "",
}
] ]
def add_spec_extras(spec): def add_spec_extras(spec):
"""Add extra documentation and features to the OpenAPI spec""" """Add extra documentation and features to the OpenAPI spec"""
# Add a list of tags, so we can control ordering and add descriptions # Add a list of tags, so we can control ordering and add descriptions

View file

@ -4,4 +4,4 @@ from .captures import *
from .instrument import * from .instrument import *
from .stage import * from .stage import *
from .streams import * from .streams import *
from .docs import * from .docs import *

View file

@ -71,8 +71,8 @@ class RAMCaptureAPI(ActionView):
args = BasicCaptureArgs() args = BasicCaptureArgs()
responses = { responses = {
200: { 200: {
"content": { "image/jpeg": {} }, "content": {"image/jpeg": {}},
"description": "A JPEG image, representing the capture" "description": "A JPEG image, representing the capture",
} }
} }

View file

@ -14,7 +14,7 @@ class LSTImageProperty(PropertyView):
responses = { responses = {
200: { 200: {
"content": { "image/jpeg": {} }, "content": {"image/jpeg": {}},
"description": "Lens-shading table in RGB format", "description": "Lens-shading table in RGB format",
} }
} }

View file

@ -146,8 +146,19 @@ class CaptureList(PropertyView):
return image_list 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): class CaptureView(View):
tags = ["captures"] tags = ["captures"]
parameters = [CAPTURE_ID_PARAMETER]
@marshal_with(FullCaptureSchema()) @marshal_with(FullCaptureSchema())
def get(self, id_): def get(self, id_):
@ -162,6 +173,8 @@ class CaptureView(View):
return capture_obj return capture_obj
get.responses = {404: {"description": "Capture object was not found"}}
def delete(self, id_): def delete(self, id_):
""" """
Delete a single image capture Delete a single image capture
@ -182,7 +195,10 @@ class CaptureView(View):
class CaptureDownload(View): class CaptureDownload(View):
tags = ["captures"] 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]): def get(self, id_, filename: Optional[str]):
""" """
@ -223,6 +239,7 @@ class CaptureDownload(View):
class CaptureTags(View): class CaptureTags(View):
tags = ["captures"] tags = ["captures"]
parameters = [CAPTURE_ID_PARAMETER]
def get(self, id_): def get(self, id_):
""" """
@ -270,6 +287,7 @@ class CaptureTags(View):
class CaptureAnnotations(View): class CaptureAnnotations(View):
tags = ["captures"] tags = ["captures"]
parameters = [CAPTURE_ID_PARAMETER]
def get(self, id_): def get(self, id_):
""" """

View file

@ -2,6 +2,7 @@ from labthings.views import View
from labthings import current_labthing from labthings import current_labthing
from flask import Response from flask import Response
class APISpecJSONView(View): class APISpecJSONView(View):
"""OpenAPI v3 documentation """OpenAPI v3 documentation
@ -11,6 +12,7 @@ class APISpecJSONView(View):
def get(self): def get(self):
return current_labthing().spec.to_dict() return current_labthing().spec.to_dict()
class APISpecYAMLView(View): class APISpecYAMLView(View):
"""OpenAPI v3 documentation """OpenAPI v3 documentation
@ -18,4 +20,4 @@ class APISpecYAMLView(View):
""" """
def get(self): def get(self):
return Response(current_labthing().spec.to_yaml(), mimetype='text/yaml') return Response(current_labthing().spec.to_yaml(), mimetype="text/yaml")

View file

@ -19,9 +19,7 @@ class MjpegStream(PropertyView):
responses = { responses = {
200: { 200: {
"content": { "content": {"multipart/x-mixed-replace": {}},
"multipart/x-mixed-replace": {}
},
"description": ( "description": (
"An MJPEG stream of camera images.\n\n" "An MJPEG stream of camera images.\n\n"
"This endpoint will serve JPEG images sequentially, \n" "This endpoint will serve JPEG images sequentially, \n"
@ -62,12 +60,7 @@ class SnapshotStream(PropertyView):
Single JPEG snapshot from the camera stream Single JPEG snapshot from the camera stream
""" """
responses = { responses = {200: {"content": {"image/jpeg": {}}, "description": "Snapshot taken"}}
200: {
"content": { "image/jpeg": {} },
"description": "Snapshot taken"
}
}
def get(self): def get(self):
""" """

View file

@ -44,7 +44,7 @@ setup(
# set up the project for development, you use those specific packages, rather than # set up the project for development, you use those specific packages, rather than
# the looser specifications given here. # the looser specifications given here.
install_requires=[ 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", "Flask ~= 1.0",
"Pillow ~= 7.2.0", "Pillow ~= 7.2.0",
"numpy ~= 1.20", "numpy ~= 1.20",
@ -68,7 +68,7 @@ setup(
# them to specific versions to enable consistent builds and testing. # them to specific versions to enable consistent builds and testing.
extras_require={ extras_require={
"dev": [ "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", "sphinxcontrib-openapi ~= 0.7",
"sphinx_rtd_theme ~=0.5.2", "sphinx_rtd_theme ~=0.5.2",
"rope ~= 0.14.0", "rope ~= 0.14.0",
@ -90,7 +90,7 @@ setup(
"console_scripts": [ "console_scripts": [
"ofm-serve=openflexure_microscope.api.app:ofm_serve", "ofm-serve=openflexure_microscope.api.app:ofm_serve",
"ofm-rescue=openflexure_microscope.rescue.auto:main", "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={ project_urls={