Sorted remaining content_type issues and blackened
This commit is contained in:
parent
38fbe530d2
commit
b7c9d3b73b
13 changed files with 114 additions and 92 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ from .captures import *
|
|||
from .instrument import *
|
||||
from .stage import *
|
||||
from .streams import *
|
||||
from .docs import *
|
||||
from .docs import *
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class LSTImageProperty(PropertyView):
|
|||
|
||||
responses = {
|
||||
200: {
|
||||
"content": { "image/jpeg": {} },
|
||||
"content": {"image/jpeg": {}},
|
||||
"description": "Lens-shading table in RGB format",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue