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)