Added basic thing description to root

This commit is contained in:
Joel Collins 2019-12-18 14:30:33 +00:00
parent d52453849c
commit f2af359b8b
24 changed files with 677 additions and 183 deletions

View file

@ -6,7 +6,7 @@ import logging
import sys
import os
from flask import Flask, jsonify, send_file
from flask import Flask, jsonify, send_file, url_for
from serial import SerialException
from datetime import datetime
@ -79,7 +79,7 @@ CORS(app, resources=r"*")
handler = JSONExceptionHandler(app)
# Attach lab devices
labthing = LabThing(app, prefix="/api/v2b")
labthing = LabThing(app, prefix="/api/v2")
labthing.description = "Test LabThing-based API for OpenFlexure Microscope"
labthing.register_device(api_microscope, "openflexure_microscope")
@ -88,41 +88,23 @@ for _plugin in find_plugins(USER_PLUGINS_PATH):
labthing.register_plugin(_plugin)
from openflexure_microscope.api.v2.views.captures import add_captures_to_labthing
add_captures_to_labthing(labthing, prefix="")
from openflexure_microscope.api.v2.views.state import add_states_to_labthing
# WEBAPP ROUTES
add_states_to_labthing(labthing, prefix="")
### V2
# Root routes
v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope)
app.register_blueprint(v2_root_blueprint, url_prefix=uri("/", "v2"))
from openflexure_microscope.api.v2.views.tasks import add_tasks_to_labthing
v2_streams_blueprint = v2.blueprints.streams.construct_blueprint(api_microscope)
app.register_blueprint(v2_streams_blueprint, url_prefix=uri("/streams", "v2"))
add_tasks_to_labthing(labthing, prefix="")
# Captures routes
v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope)
app.register_blueprint(v2_captures_blueprint, url_prefix=uri("/captures", "v2"))
from openflexure_microscope.api.v2.views.streams import add_streams_to_labthing
# Settings routes
v2_settings_blueprint = v2.blueprints.settings.construct_blueprint(api_microscope)
app.register_blueprint(v2_settings_blueprint, url_prefix=uri("/settings", "v2"))
add_streams_to_labthing(labthing, prefix="")
# Status routes
v2_status_blueprint = v2.blueprints.status.construct_blueprint(api_microscope)
app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2"))
from openflexure_microscope.api.v2.views.actions import add_actions_to_labthing
# Plugins routes
# v2_plugin_blueprint = v2.blueprints.plugins.construct_blueprint(api_microscope)
# app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2"))
# Tasks routes
v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope)
app.register_blueprint(v2_tasks_blueprint, url_prefix=uri("/tasks", "v2"))
# Actions routes
v2_actions_blueprint = v2.blueprints.actions.construct_blueprint(api_microscope)
app.register_blueprint(v2_actions_blueprint, url_prefix=uri("/actions", "v2"))
add_actions_to_labthing(labthing)
@app.route("/routes")
@ -139,6 +121,26 @@ def routes():
return jsonify(list_routes(app))
@app.route("/props")
def props():
p = {}
for key, prop in labthing.properties.items():
p[key] = {}
p[key]["view"] = prop
p[key]["url"] = labthing.url_for(prop, _external=True)
return jsonify(p)
@app.route("/ac")
def actions():
p = {}
for key, prop in labthing.actions.items():
p[key] = {}
p[key]["view"] = prop
p[key]["url"] = labthing.url_for(prop, _external=True)
return jsonify(p)
@app.route("/log")
def err_log():
"""

View file

@ -1 +1 @@
from . import root, captures, settings, status, tasks, streams, plugins, actions
from . import root, captures, settings, status, tasks, streams, actions

View file

@ -1,114 +0,0 @@
"""
Top-level representation of attached and enabled plugins
"""
from openflexure_microscope.api.views import MicroscopeViewPlugin
from openflexure_microscope.utilities import get_docstring, description_from_view
from openflexure_microscope.api.utilities import blueprint_for_module
from openflexure_microscope.common.labthings.plugins import find_plugins
from openflexure_microscope.config import USER_PLUGINS_PATH
from flask import Blueprint, jsonify, url_for
from openflexure_microscope.api.views import MicroscopeView
import copy
import logging
import warnings
_plugins = find_plugins(USER_PLUGINS_PATH)
print(_plugins)
def plugins_representation(plugin_list):
"""
Generate a dictionary representation of all plugins, including Flask route URLs
Args:
plugin_loader_object (:py:class:`openflexure_microscope.plugins.PluginLoader`): Microscope plugin loader
Returns:
dict: Dictionary representation of all plugins
"""
plugins = {}
for plugin in plugin_list:
logging.debug(f"Representing plugin {plugin._name}")
d = {
"python_name": plugin._name_python_safe,
"plugin": str(plugin),
"views": {},
"gui": plugin.gui,
"description": get_docstring(plugin),
}
for view_id, view_data in plugin.views.items():
logging.debug(f"Representing view {view_id}")
uri = url_for(f"v2_plugins_blueprint.plugins") + view_data["rule"][1:]
# uri = view_data["rule"]
# Make links dictionary if it doesn't yet exist
view_d = {"links": {"self": uri}}
view_d.update(description_from_view(view_data["view"]))
d["views"][view_id] = view_d
plugins[plugin._name] = d
return plugins
class PluginFormAPI(MicroscopeView):
def get(self):
"""
Return the current plugin forms
.. :quickref: Plugin; Get forms
Returns an array of present plugin forms (describing plugin user interfaces.)
Please note, this is *not* a list of all enabled plugins, only those with associated
user interface forms.
A complete list of enabled plugins can be found in the microscope state.
"""
global _plugins
return jsonify(plugins_representation(_plugins))
def construct_blueprint(microscope_obj):
blueprint = blueprint_for_module(__name__)
for plugin_obj in _plugins:
for plugin_view_id, plugin_view in plugin_obj.views.items():
# Add route to the plugins blueprint
blueprint.add_url_rule(
plugin_view["rule"],
view_func=plugin_view["view"].as_view(
f"{plugin_obj._name_python_safe}_{plugin_view_id}"
),
**plugin_view["kwargs"],
)
# Create a base route to return plugin API forms, if any exist
blueprint.add_url_rule(
"/", view_func=PluginFormAPI.as_view("plugins", microscope=microscope_obj)
)
# For each plugin attached to the microscope object
for plugin in microscope_obj.plugins.active:
for plugin_view_id, plugin_view in plugin.views.items():
# Add route to the plugins blueprint
blueprint.add_url_rule(
plugin_view["rule"],
view_func=plugin_view["view"].as_view(
f"{plugin._name_python_safe}_{plugin_view_id}",
microscope=microscope_obj,
plugin=plugin,
),
**plugin_view["kwargs"],
)
return blueprint

View file

@ -7,7 +7,6 @@ from openflexure_microscope.api.utilities import blueprint_name_for_module
from openflexure_microscope.api.v2.blueprints import (
settings,
status,
plugins,
captures,
actions,
streams,

View file

@ -0,0 +1,65 @@
"""
Top-level representation of enabled actions
"""
from flask import Blueprint, url_for, jsonify
from openflexure_microscope.api.utilities import blueprint_for_module
from openflexure_microscope.utilities import get_docstring, description_from_view
from openflexure_microscope.api.views import MicroscopeView
from . import camera, stage, system
_actions = {
"capture": {
"rule": "/camera/capture/",
"view_class": camera.CaptureAPI,
"conditions": True,
},
"previewStart": {
"rule": "/camera/preview/start",
"view_class": camera.GPUPreviewStartAPI,
"conditions": True,
},
"previewStop": {
"rule": "/camera/preview/stop",
"view_class": camera.GPUPreviewStopAPI,
"conditions": True,
},
"move": {
"rule": "/stage/move/",
"view_class": stage.MoveStageAPI,
"conditions": True,
},
"zeroStage": {
"rule": "/stage/zero/",
"view_class": stage.ZeroStageAPI,
"conditions": True,
},
"shutdown": {
"rule": "/system/shutdown/",
"view_class": system.ShutdownAPI,
"conditions": system.is_raspberrypi(),
},
"reboot": {
"rule": "/system/reboot/",
"view_class": system.RebootAPI,
"conditions": system.is_raspberrypi(),
},
}
def enabled_actions():
global _actions
return {k: v for k, v in _actions.items() if v["conditions"]}
def add_actions_to_labthing(labthing, prefix=""):
"""
Add all capture resources to a labthing
"""
for name, action in enabled_actions().items():
view_class = action["view_class"]
rule = action["rule"]
labthing.add_resource(view_class, f"{prefix}/actions{rule}")
labthing.register_action(view_class)

View file

@ -0,0 +1,82 @@
from openflexure_microscope.api.utilities import get_bool, JsonResponse
from openflexure_microscope.common.labthings.resource import Resource
from openflexure_microscope.common.labthings.find import find_device
from openflexure_microscope.utilities import filter_dict
from openflexure_microscope.api.v2.views.captures import capture_schema
import logging
from flask import jsonify, request, abort, url_for, redirect, send_file
class CaptureAPI(Resource):
"""
Create a new image capture.
"""
def post(self):
microscope = find_device("openflexure_microscope")
payload = JsonResponse(request)
filename = payload.param("filename")
temporary = payload.param("temporary", default=False, convert=bool)
use_video_port = payload.param("use_video_port", default=False, convert=bool)
bayer = payload.param("bayer", default=True, convert=bool)
metadata = payload.param("metadata", default={}, convert=dict)
tags = payload.param("tags", default=[], convert=list)
resize = payload.param("size", default=None)
if resize:
if ("width" in resize) and ("height" in resize):
resize = (
int(resize["width"]),
int(resize["height"]),
) # Convert dict to tuple
else:
abort(404)
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
with microscope.camera.lock:
output = microscope.camera.new_image(temporary=temporary, filename=filename)
microscope.camera.capture(
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
)
# Inject system metadata
output.put_metadata(microscope.metadata, system=True)
# Insert custom metadata
output.put_metadata(metadata)
# Insert custom tags
output.put_tags(tags)
return capture_schema.jsonify(output)
class GPUPreviewStartAPI(Resource):
def post(self):
microscope = find_device("openflexure_microscope")
payload = JsonResponse(request)
window = payload.param("window", default=[])
logging.debug(window)
if len(window) != 4:
fullscreen = True
window = None
else:
fullscreen = False
window = [int(w) for w in window]
microscope.camera.start_preview(fullscreen=fullscreen, window=window)
return jsonify(microscope.state)
class GPUPreviewStopAPI(Resource):
def post(self):
microscope = find_device("openflexure_microscope")
microscope.camera.stop_preview()
return jsonify(microscope.state)

View file

@ -0,0 +1,55 @@
from openflexure_microscope.api.utilities import JsonResponse
from openflexure_microscope.common.labthings.resource import Resource
from openflexure_microscope.common.labthings.find import find_device
from openflexure_microscope.utilities import axes_to_array, filter_dict
from flask import Blueprint, jsonify, request
import logging
class MoveStageAPI(Resource):
def post(self):
microscope = find_device("openflexure_microscope")
# Create response object
payload = JsonResponse(request)
logging.debug(payload.json)
# Handle absolute positioning (calculate a relative move from current position and target)
if (payload.param("absolute") is True) and (
microscope.stage
): # Only if stage exists
target_position = axes_to_array(payload.json, ["x", "y", "z"])
logging.debug("TARGET: {}".format(target_position))
position = [
target_position[i] - microscope.stage.position[i] for i in range(3)
]
logging.debug("DELTA: {}".format(position))
else:
# Get coordinates from payload
position = axes_to_array(payload.json, ["x", "y", "z"], [0, 0, 0])
logging.debug(position)
# Move if stage exists
if microscope.stage:
# Explicitally acquire lock
with microscope.stage.lock:
microscope.stage.move_rel(position)
else:
logging.warning("Unable to move. No stage found.")
return jsonify(microscope.status["stage"]["position"])
class ZeroStageAPI(Resource):
"""
Zero stage coordinates
"""
def post(self):
microscope = find_device("openflexure_microscope")
microscope.stage.zero_position()
return jsonify(microscope.status["stage"])

View file

@ -0,0 +1,46 @@
from openflexure_microscope.common.labthings.resource import Resource
import subprocess
import os
from sys import platform
def is_raspberrypi(raise_on_errors=False):
"""
Checks if Raspberry Pi.
"""
# I mean, if it works, it works...
return os.path.exists("/usr/bin/raspi-config")
class ShutdownAPI(Resource):
"""
Attempt to shutdown the device
"""
def post(self):
"""
Attempt to shutdown the device
.. :quickref: Actions; Shutdown
"""
subprocess.Popen(["sudo", "shutdown", "-h", "now"])
return "{}", 201
class RebootAPI(Resource):
"""
Attempt to reboot the device
"""
def post(self):
"""
Attempt to shutdown the device
.. :quickref: Actions; Shutdown
"""
subprocess.Popen(["sudo", "shutdown", "-r", "now"])
return "{}", 201

View file

@ -33,9 +33,11 @@ class CaptureSchema(Schema):
"mimetype": "application/json",
},
"download": {
"href": fields.AbsoluteUrlFor("CaptureDownload", id="<id>", filename="<filename>"),
"href": fields.AbsoluteUrlFor(
"CaptureDownload", id="<id>", filename="<filename>"
),
"mimetype": "image/jpeg",
}
},
}
)
@ -185,11 +187,14 @@ def add_captures_to_labthing(labthing, prefix=""):
Add all capture resources to a labthing
"""
labthing.add_resource(CaptureList, f"{prefix}/captures", endpoint="CaptureList")
labthing.register_property(CaptureList)
labthing.add_resource(
CaptureResource, f"{prefix}/captures/<id>", endpoint="CaptureResource"
)
labthing.add_resource(
CaptureDownload, f"{prefix}/captures/<id>/download/<filename>", endpoint="CaptureDownload"
CaptureDownload,
f"{prefix}/captures/<id>/download/<filename>",
endpoint="CaptureDownload",
)
labthing.add_resource(
CaptureTags, f"{prefix}/captures/<id>/tags", endpoint="CaptureTags"

View file

@ -0,0 +1,94 @@
from openflexure_microscope.api.utilities import JsonResponse
from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path
from openflexure_microscope.common.labthings.find import find_device
from openflexure_microscope.common.labthings.resource import Resource
from flask import jsonify, request, abort
import logging
class SettingsProperty(Resource):
def get(self):
microscope = find_device("openflexure_microscope")
return jsonify(microscope.read_settings())
def put(self):
microscope = find_device("openflexure_microscope")
payload = JsonResponse(request)
logging.debug("Updating settings from PUT request:")
logging.debug(payload.json)
microscope.apply_settings(payload.json)
microscope.save_settings()
return self.get()
class NestedSettingsProperty(Resource):
def get(self, route):
microscope = find_device("openflexure_microscope")
keys = route.split("/")
try:
value = get_by_path(microscope.read_settings(), keys)
except KeyError:
return abort(404)
return jsonify(value)
def put(self, route):
microscope = find_device("openflexure_microscope")
keys = route.split("/")
payload = JsonResponse(request)
dictionary = create_from_path(keys)
set_by_path(dictionary, keys, payload.json)
microscope.apply_settings(dictionary)
microscope.save_settings()
return self.get(route)
class StatusProperty(Resource):
def get(self):
microscope = find_device("openflexure_microscope")
return jsonify(microscope.status)
class NestedStatusProperty(Resource):
def get(self, route):
microscope = find_device("openflexure_microscope")
keys = route.split("/")
try:
value = get_by_path(microscope.status, keys)
except KeyError:
return abort(404)
return jsonify(value)
def add_states_to_labthing(labthing, prefix=""):
"""
Add all settings and status resources to a labthing
"""
labthing.add_resource(
SettingsProperty, f"{prefix}/settings", endpoint="SettingsProperty"
)
labthing.register_property(SettingsProperty)
labthing.add_resource(
NestedSettingsProperty,
f"{prefix}/settings/<path:route>",
endpoint="NestedSettingsProperty",
)
labthing.add_resource(StatusProperty, f"{prefix}/status", endpoint="StatusProperty")
labthing.register_property(StatusProperty)
labthing.add_resource(
NestedStatusProperty,
f"{prefix}/status/<path:route>",
endpoint="NestedStatusProperty",
)

View file

@ -0,0 +1,62 @@
from openflexure_microscope.api.utilities import gen, JsonResponse
from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path
from openflexure_microscope.common.labthings.find import find_device
from openflexure_microscope.common.labthings.resource import Resource
from flask import jsonify, request, abort, Response
import logging
class MjpegStream(Resource):
"""
Real-time MJPEG stream from the microscope camera
"""
def get(self):
"""
Real-time MJPEG stream from the microscope camera
"""
microscope = find_device("openflexure_microscope")
# Restart stream worker thread
microscope.camera.start_worker()
return Response(
gen(microscope.camera), mimetype="multipart/x-mixed-replace; boundary=frame"
)
class SnapshotStream(Resource):
"""
Single JPEG snapshot from the camera stream
"""
def get(self):
"""
Single snapshot from the camera stream
.. :quickref: Streams; Camera snapshot
:>header Accept: image/jpeg
:>header Content-Type: image/jpeg
:status 200: stream active
"""
microscope = find_device("openflexure_microscope")
# Restart stream worker thread
microscope.camera.start_worker()
return Response(microscope.camera.get_frame(), mimetype="image/jpeg")
def add_streams_to_labthing(labthing, prefix=""):
"""
Add all stream resources to a labthing
"""
labthing.add_resource(
MjpegStream, f"{prefix}/streams/mjpeg", endpoint="MjpegStream"
)
labthing.register_property(MjpegStream)
labthing.add_resource(
SnapshotStream, f"{prefix}/streams/snapshot", endpoint="SnapshotStream"
)
labthing.register_property(SnapshotStream)

View file

@ -0,0 +1,71 @@
import logging
from flask import abort, request, redirect, url_for, send_file, jsonify
from openflexure_microscope.api.utilities import get_bool, JsonResponse
from openflexure_microscope.common.labthings.schema import Schema
from openflexure_microscope.common.labthings import fields
from openflexure_microscope.common.labthings.resource import Resource
from openflexure_microscope.common import tasks
class TaskSchema(Schema):
_ID = fields.String(data_key="id")
target_string = fields.String(data_key="function")
_status = fields.String(data_key="status")
progress = fields.String()
data = fields.Raw()
_return_value = fields.Raw(data_key="return")
_start_time = fields.String(data_key="start_time")
_end_time = fields.String(data_key="end_time")
# TODO: Add HTTP methods
links = fields.Hyperlinks(
{
"self": {
"href": fields.AbsoluteUrlFor("TaskResource", id="<id>"),
"mimetype": "application/json",
}
}
)
task_schema = TaskSchema()
task_list_schema = TaskSchema(many=True)
class TaskList(Resource):
def get(self):
return task_list_schema.jsonify(tasks.tasks())
class TaskResource(Resource):
def get(self, id):
try:
task = tasks.dict()[id]
except KeyError:
return abort(404) # 404 Not Found
# Return task state
return jsonify(task)
def delete(self, id):
try:
task = tasks.dict()[id]
except KeyError:
return abort(404) # 404 Not Found
task.terminate()
return jsonify(task.state)
def add_tasks_to_labthing(labthing, prefix=""):
"""
Add all settings and status resources to a labthing
"""
labthing.add_resource(TaskList, f"{prefix}/tasks", endpoint="TasksProperty")
labthing.register_property(TaskList)
labthing.add_resource(TaskResource, f"{prefix}/tasks/<id>", endpoint="TaskResource")

View file

@ -150,4 +150,4 @@ class Hyperlinks(Field):
Field.__init__(self, **kwargs)
def _serialize(self, value, attr, obj):
return _rapply(self.schema, _url_val, key=attr, obj=obj)
return _rapply(self.schema, _url_val, key=attr, obj=obj)

View file

@ -4,7 +4,7 @@ from flask import current_app
from . import EXTENSION_NAME
def _current_labthing():
def current_labthing():
app = current_app._get_current_object()
if not app:
return None
@ -17,31 +17,32 @@ def _current_labthing():
def registered_plugins(labthing_instance=None):
if not labthing_instance:
labthing_instance = _current_labthing()
labthing_instance = current_labthing()
return labthing_instance.plugins
def registered_devices(labthing_instance=None):
if not labthing_instance:
labthing_instance = _current_labthing()
labthing_instance = current_labthing()
return labthing_instance.devices
def find_device(device_name, labthing_instance=None):
if not labthing_instance:
labthing_instance = _current_labthing()
labthing_instance = current_labthing()
if device_name in labthing_instance.devices:
return labthing_instance.devices[device_name]
else:
return None
def find_plugin(plugin_name, labthing_instance=None):
if not labthing_instance:
labthing_instance = _current_labthing()
labthing_instance = current_labthing()
logging.debug("Current labthing:")
logging.debug(_current_labthing())
logging.debug(current_labthing())
if plugin_name in labthing_instance.plugins:
return labthing_instance.plugins[plugin_name]

View file

@ -1,13 +1,17 @@
from flask import current_app, _app_ctx_stack, request
from flask import current_app, _app_ctx_stack, request, url_for, jsonify
from .plugins import BasePlugin
from .views.plugins import PluginListResource
from .resource import Resource
from ..utilities import get_docstring
from . import EXTENSION_NAME
class LabThing(object):
def __init__(self, app=None, prefix="", description=""):
def __init__(self, app=None, prefix="", title="", description=""):
self.app = app
self.devices = {}
@ -15,10 +19,14 @@ class LabThing(object):
self.plugins = {}
self.resources = []
self.properties = {}
self.actions = {}
self.endpoints = set()
self.url_prefix = prefix
self.description = description
self.title = title
if app is not None:
self.init_app(app)
@ -33,10 +41,17 @@ class LabThing(object):
self._create_base_routes()
if len(self.resources) > 0:
for resource, urls, endpoint, kwargs in self.resources:
self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs)
def teardown(self, exception):
print(f"Tearing down devices: {self.devices}")
def _create_base_routes(self):
# Add thing description to root
self.app.add_url_rule(self._complete_url("/", ""), "td", self.td)
# Add plugin overview
self.add_resource(PluginListResource, "/plugins")
### Device stuff
@ -51,8 +66,6 @@ class LabThing(object):
else:
raise TypeError("Plugin object must be an instance of BasePlugin")
# TODO: Add plugin routes
for plugin_view_id, plugin_view in plugin_object.views.items():
# Add route to the plugins blueprint
self.add_resource(
@ -61,6 +74,12 @@ class LabThing(object):
**plugin_view["kwargs"],
)
for prop in plugin_object.properties:
self.register_property(prop)
for action in plugin_object.actions:
self.register_action(action)
### Resource stuff
def _complete_url(self, url_part, registration_prefix):
@ -73,7 +92,23 @@ class LabThing(object):
parts = [registration_prefix, self.url_prefix, url_part]
return "".join([part for part in parts if part])
def add_resource(self, resource, *urls, **kwargs):
def register_property(self, resource):
if hasattr(resource, "endpoint"):
self.properties[resource.endpoint] = resource
else:
raise RuntimeError(
f"Resource {resource} has not yet been added. Cannot set as a property."
)
def register_action(self, resource):
if hasattr(resource, "endpoint"):
self.actions[resource.endpoint] = resource
else:
raise RuntimeError(
f"Resource {resource} has not yet been added. Cannot set as an action."
)
def add_resource(self, resource, *urls, endpoint=None, **kwargs):
"""Adds a resource to the api.
:param resource: the class name of your resource
:type resource: :class:`Type[Resource]`
@ -81,7 +116,7 @@ class LabThing(object):
flask routing rules apply. Any url variables will be
passed to the resource method as args.
:type urls: str
:param endpoint: endpoint name (defaults to :meth:`Resource.__name__.lower`
:param endpoint: endpoint name (defaults to :meth:`Resource.__name__`
Can be used to reference this route in :class:`fields.Url` fields
:type endpoint: str
:param resource_class_args: args to be forwarded to the constructor of
@ -97,10 +132,11 @@ class LabThing(object):
api.add_resource(Foo, '/foo', endpoint="foo")
api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
"""
endpoint = endpoint or resource.__name__
if self.app is not None:
self._register_view(self.app, resource, *urls, **kwargs)
self._register_view(self.app, resource, *urls, endpoint=endpoint, **kwargs)
else:
self.resources.append((resource, urls, kwargs))
self.resources.append((resource, urls, endpoint, kwargs))
def resource(self, *urls, **kwargs):
"""Wraps a :class:`~flask_restful.Resource` class, adding it to the
@ -120,8 +156,8 @@ class LabThing(object):
return decorator
def _register_view(self, app, resource, *urls, **kwargs):
endpoint = kwargs.pop("endpoint", None) or resource.__name__.lower()
def _register_view(self, app, resource, *urls, endpoint=None, **kwargs):
endpoint = endpoint or resource.__name__
self.endpoints.add(endpoint)
resource_class_args = kwargs.pop("resource_class_args", ())
resource_class_kwargs = kwargs.pop("resource_class_kwargs", {})
@ -147,3 +183,36 @@ class LabThing(object):
rule = self._complete_url(url, "")
# Add the url to the application or blueprint
app.add_url_rule(rule, view_func=resource_func, **kwargs)
### Utilities
def url_for(self, resource, **values):
"""Generates a URL to the given resource.
Works like :func:`flask.url_for`."""
endpoint = resource.endpoint
return url_for(endpoint, **values)
### Description
def td(self):
props = {}
for key, prop in self.properties.items():
props[key] = {}
props[key]["title"] = prop.__name__
props[key]["description"] = get_docstring(prop)
props[key]["links"] = [{"href": self.url_for(prop, _external=True)}]
actions = {}
for key, prop in self.actions.items():
actions[key] = {}
actions[key]["title"] = prop.__name__
actions[key]["description"] = get_docstring(prop)
actions[key]["links"] = [{"href": self.url_for(prop, _external=True)}]
td = {
"title": self.title,
"description": self.description,
"properties": props,
"actions": actions,
}
return jsonify(td)

View file

@ -27,6 +27,9 @@ class BasePlugin:
self._rules = {} # Key: Original rule. Val: View class
self._gui = None
self.actions = []
self.properties = []
self.name = name
self.methods = {}
@ -35,7 +38,7 @@ class BasePlugin:
def views(self):
return self._views
def add_view(self, rule, view_class, **kwargs):
def add_view(self, view_class, rule, **kwargs):
# Remove all leading slashes from view route
cleaned_rule = rule
while cleaned_rule[0] == "/":
@ -57,6 +60,12 @@ class BasePlugin:
# Store the rule expansion information
self._rules[rule] = self._views[view_id]
def register_action(self, view_class):
self.actions.append(view_class)
def register_property(self, view_class):
self.properties.append(view_class)
@property
def gui(self):
print(self._gui)
@ -104,13 +113,16 @@ class BasePlugin:
def _name_uri_safe(self):
return snake_to_spine(self._name_python_safe)
def add_method(self, method_name, method):
def add_method(self, method, method_name):
self.methods[method_name] = method
if not hasattr(self, method_name):
setattr(self, method_name, method)
else:
logging.warning("Unable to bind method to plugin. Method name already exists.")
logging.warning(
"Unable to bind method to plugin. Method name already exists."
)
def find_plugins(plugin_path, module_name="plugins"):
print(f"Loading plugins from {plugin_path}")

View file

@ -1,6 +1,7 @@
__all__ = [
"taskify",
"tasks",
"dict",
"states",
"current_task",
"update_task_progress",
@ -12,6 +13,7 @@ __all__ = [
from .pool import (
tasks,
dict,
states,
current_task,
update_task_progress,

View file

@ -6,12 +6,21 @@ from .thread import TaskThread
from flask import copy_current_request_context
class TaskMaster:
def __init__(self, *args, **kwargs):
self._tasks = []
@property
def tasks(self):
"""
Returns:
list: List of TaskThread objects.
"""
return self._tasks
@property
def dict(self):
"""
Returns:
dict: Dictionary of TaskThread objects. Key is TaskThread ID.
@ -28,7 +37,9 @@ class TaskMaster:
def new(self, f, *args, **kwargs):
# copy_current_request_context allows threads to access flask current_app
task = TaskThread(target=copy_current_request_context(f), args=args, kwargs=kwargs)
task = TaskThread(
target=copy_current_request_context(f), args=args, kwargs=kwargs
)
self._tasks.append(task)
return task
@ -47,13 +58,23 @@ class TaskMaster:
def tasks():
"""
List of tasks in default taskmaster
Returns:
list: List of tasks in default taskmaster
"""
global _default_task_master
return _default_task_master.tasks
def dict():
"""
Dictionary of tasks in default taskmaster
Returns:
dict: Dictionary of tasks in default taskmaster
"""
global _default_task_master
return _default_task_master.tasks
return _default_task_master.dict
def states():

View file

@ -0,0 +1,6 @@
def get_docstring(obj):
ds = obj.__doc__
if ds:
return ds.strip()
else:
return ""

View file

@ -147,7 +147,6 @@ class Microscope:
else:
return False
# Create unified status
@property
def status(self):
@ -239,7 +238,7 @@ class Microscope:
"""
system_metadata = {
"microscope_settings": self.read_settings(),
"microscope_state": self.state,
"microscope_state": self.status,
"microscope_id": self.id,
"microscope_name": self.name,
}

View file

@ -356,9 +356,10 @@ class FastAutofocusAPI(MethodView):
autofocus_plugin_v2 = BasePlugin("autofocus")
autofocus_plugin_v2.add_method("fast_autofocus", fast_autofocus)
autofocus_plugin_v2.add_method("autofocus", autofocus)
autofocus_plugin_v2.add_method(fast_autofocus, "fast_autofocus")
autofocus_plugin_v2.add_method(autofocus, "autofocus")
autofocus_plugin_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness")
autofocus_plugin_v2.add_view(AutofocusAPI, "/autofocus")
autofocus_plugin_v2.add_view(FastAutofocusAPI, "/fast_autofocus")
autofocus_plugin_v2.add_view("/measure_sharpness", MeasureSharpnessAPI)
autofocus_plugin_v2.add_view("/autofocus", AutofocusAPI)
autofocus_plugin_v2.add_view("/fast_autofocus", FastAutofocusAPI)

View file

@ -25,6 +25,7 @@ import time
### Grid construction
def construct_grid(initial, step_sizes, n_steps, style="raster"):
"""
Given an initial position, step sizes, and number of steps,
@ -64,6 +65,7 @@ def flatten_grid(grid):
_images_to_be_captured: int = 1
_images_captured_so_far: int = 0
def progress():
progress = (_images_captured_so_far / _images_to_be_captured) * 100
logging.info(progress)
@ -72,6 +74,7 @@ def progress():
### Capturing
def capture(
microscope,
basename,
@ -114,6 +117,7 @@ def capture(
### Scanning
def tile(
microscope,
basename: str = None,
@ -181,9 +185,7 @@ def tile(
) # shorthand for Z stack range
# Construct an x-y grid (worry about z later)
x_y_grid = construct_grid(
initial_position, step_size[:2], grid[:2], style=style
)
x_y_grid = construct_grid(initial_position, step_size[:2], grid[:2], style=style)
# Keep the initial Z position the same as our current position
next_z = initial_position[2]
@ -270,6 +272,7 @@ def tile(
logging.debug("Returning to {}".format(initial_position))
microscope.stage.move_abs(initial_position)
def stack(
microscope,
basename: str = None,
@ -336,6 +339,7 @@ def stack(
### Web views
class TileScanAPI(MethodView):
def post(self):
payload = JsonResponse(request)
@ -396,4 +400,5 @@ class TileScanAPI(MethodView):
scan_plugin_v2 = BasePlugin("scan")
scan_plugin_v2.add_view("/tile", TileScanAPI)
scan_plugin_v2.add_view(TileScanAPI, "/tile")
scan_plugin_v2.register_action(TileScanAPI)

14
poetry.lock generated
View file

@ -190,6 +190,14 @@ optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]]
category = "main"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
name = "marshmallow"
optional = false
python-versions = ">=3.5"
version = "3.3.0"
[[package]]
category = "dev"
description = "McCabe checker, plugin for flake8"
@ -230,6 +238,7 @@ version = "1.13.1b0"
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
type = "git"
url = "https://github.com/rwb27/picamera.git"
[[package]]
category = "main"
description = "Python Imaging Library (Fork)"
@ -473,7 +482,7 @@ version = "1.11.2"
rpi = ["picamera", "RPi.GPIO"]
[metadata]
content-hash = "ea3fcd1909a76b04efa8ebefdcb77f0b449642820a94429fbbbf20bb4f9a64ab"
content-hash = "254e455bd1b5a5482f296f8afccf5bd10d109206e8350b9e8ec304576e0d0ae5"
python-versions = "^3.6"
[metadata.hashes]
@ -497,6 +506,7 @@ itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f1
jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"]
lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"]
markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
marshmallow = ["0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e", "3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"]
mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
numpy = ["0a7a1dd123aecc9f0076934288ceed7fd9a81ba3919f11a855a7887cbe82a02f", "0c0763787133dfeec19904c22c7e358b231c87ba3206b211652f8cbe1241deb6", "3d52298d0be333583739f1aec9026f3b09fdfe3ddf7c7028cb16d9d2af1cca7e", "43bb4b70585f1c2d153e45323a886839f98af8bfa810f7014b20be714c37c447", "475963c5b9e116c38ad7347e154e5651d05a2286d86455671f5b1eebba5feb76", "64874913367f18eb3013b16123c9fed113962e75d809fca5b78ebfbb73ed93ba", "683828e50c339fc9e68720396f2de14253992c495fdddef77a1e17de55f1decc", "6ca4000c4a6f95a78c33c7dadbb9495c10880be9c89316aa536eac359ab820ae", "75fd817b7061f6378e4659dd792c84c0b60533e867f83e0d1e52d5d8e53df88c", "7d81d784bdbed30137aca242ab307f3e65c8d93f4c7b7d8f322110b2e90177f9", "8d0af8d3664f142414fd5b15cabfd3b6cc3ef242a3c7a7493257025be5a6955f", "9679831005fb16c6df3dd35d17aa31dc0d4d7573d84f0b44cc481490a65c7725", "a8f67ebfae9f575d85fa859b54d3bdecaeece74e3274b0b5c5f804d7ca789fe1", "acbf5c52db4adb366c064d0b7c7899e3e778d89db585feadd23b06b587d64761", "ada4805ed51f5bcaa3a06d3dd94939351869c095e30a2b54264f5a5004b52170", "c7354e8f0eca5c110b7e978034cd86ed98a7a5ffcf69ca97535445a595e07b8e", "e2e9d8c87120ba2c591f60e32736b82b67f72c37ba88a4c23c81b5b8fa49c018", "e467c57121fe1b78a8f68dd9255fbb3bb3f4f7547c6b9e109f31d14569f490c3", "ede47b98de79565fcd7f2decb475e2dcc85ee4097743e551fe26cfc7eb3ff143", "f58913e9227400f1395c7b800503ebfdb0772f1c33ff8cb4d6451c06cabdf316", "fe39f5fd4103ec4ca3cb8600b19216cd1ff316b4990f4c0b6057ad982c0a34d5"]
packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"]
@ -522,7 +532,7 @@ sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c
sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"]
sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"]
typed-ast = ["18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"]
urllib3 = ["a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", "f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"]
werkzeug = ["7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", "e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"]
wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"]

View file

@ -33,6 +33,7 @@ scipy = "1.3.0" # Exact version until we can guarantee a wheel
picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch.
"RPi.GPIO" = { version = "^0.6.5", optional = true }
marshmallow = "^3.3"
[tool.poetry.extras]
rpi = ["picamera", "RPi.GPIO"]