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
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,
}
]
]

View file

@ -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):
"""

View file

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

View file

@ -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()

View file

@ -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()}
return {"sharpness": self.extension.measure_sharpness()}

View file

@ -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

View file

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

View file

@ -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",
}
}

View file

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

View file

@ -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_):
"""

View file

@ -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')
return Response(current_labthing().spec.to_yaml(), mimetype="text/yaml")

View file

@ -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):
"""

View file

@ -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={