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 sys
import os import os
from flask import Flask, jsonify, send_file from flask import Flask, jsonify, send_file, url_for
from serial import SerialException from serial import SerialException
from datetime import datetime from datetime import datetime
@ -79,7 +79,7 @@ CORS(app, resources=r"*")
handler = JSONExceptionHandler(app) handler = JSONExceptionHandler(app)
# Attach lab devices # 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.description = "Test LabThing-based API for OpenFlexure Microscope"
labthing.register_device(api_microscope, "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) labthing.register_plugin(_plugin)
from openflexure_microscope.api.v2.views.captures import add_captures_to_labthing from openflexure_microscope.api.v2.views.captures import add_captures_to_labthing
add_captures_to_labthing(labthing, prefix="") 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 from openflexure_microscope.api.v2.views.tasks import add_tasks_to_labthing
# Root routes
v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope)
app.register_blueprint(v2_root_blueprint, url_prefix=uri("/", "v2"))
v2_streams_blueprint = v2.blueprints.streams.construct_blueprint(api_microscope) add_tasks_to_labthing(labthing, prefix="")
app.register_blueprint(v2_streams_blueprint, url_prefix=uri("/streams", "v2"))
# Captures routes from openflexure_microscope.api.v2.views.streams import add_streams_to_labthing
v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope)
app.register_blueprint(v2_captures_blueprint, url_prefix=uri("/captures", "v2"))
# Settings routes add_streams_to_labthing(labthing, prefix="")
v2_settings_blueprint = v2.blueprints.settings.construct_blueprint(api_microscope)
app.register_blueprint(v2_settings_blueprint, url_prefix=uri("/settings", "v2"))
# Status routes from openflexure_microscope.api.v2.views.actions import add_actions_to_labthing
v2_status_blueprint = v2.blueprints.status.construct_blueprint(api_microscope)
app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2"))
# Plugins routes add_actions_to_labthing(labthing)
# 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"))
@app.route("/routes") @app.route("/routes")
@ -139,6 +121,26 @@ def routes():
return jsonify(list_routes(app)) 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") @app.route("/log")
def err_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 ( from openflexure_microscope.api.v2.blueprints import (
settings, settings,
status, status,
plugins,
captures, captures,
actions, actions,
streams, 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", "mimetype": "application/json",
}, },
"download": { "download": {
"href": fields.AbsoluteUrlFor("CaptureDownload", id="<id>", filename="<filename>"), "href": fields.AbsoluteUrlFor(
"CaptureDownload", id="<id>", filename="<filename>"
),
"mimetype": "image/jpeg", "mimetype": "image/jpeg",
} },
} }
) )
@ -185,11 +187,14 @@ def add_captures_to_labthing(labthing, prefix=""):
Add all capture resources to a labthing Add all capture resources to a labthing
""" """
labthing.add_resource(CaptureList, f"{prefix}/captures", endpoint="CaptureList") labthing.add_resource(CaptureList, f"{prefix}/captures", endpoint="CaptureList")
labthing.register_property(CaptureList)
labthing.add_resource( labthing.add_resource(
CaptureResource, f"{prefix}/captures/<id>", endpoint="CaptureResource" CaptureResource, f"{prefix}/captures/<id>", endpoint="CaptureResource"
) )
labthing.add_resource( labthing.add_resource(
CaptureDownload, f"{prefix}/captures/<id>/download/<filename>", endpoint="CaptureDownload" CaptureDownload,
f"{prefix}/captures/<id>/download/<filename>",
endpoint="CaptureDownload",
) )
labthing.add_resource( labthing.add_resource(
CaptureTags, f"{prefix}/captures/<id>/tags", endpoint="CaptureTags" 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

@ -4,7 +4,7 @@ from flask import current_app
from . import EXTENSION_NAME from . import EXTENSION_NAME
def _current_labthing(): def current_labthing():
app = current_app._get_current_object() app = current_app._get_current_object()
if not app: if not app:
return None return None
@ -17,31 +17,32 @@ def _current_labthing():
def registered_plugins(labthing_instance=None): def registered_plugins(labthing_instance=None):
if not labthing_instance: if not labthing_instance:
labthing_instance = _current_labthing() labthing_instance = current_labthing()
return labthing_instance.plugins return labthing_instance.plugins
def registered_devices(labthing_instance=None): def registered_devices(labthing_instance=None):
if not labthing_instance: if not labthing_instance:
labthing_instance = _current_labthing() labthing_instance = current_labthing()
return labthing_instance.devices return labthing_instance.devices
def find_device(device_name, labthing_instance=None): def find_device(device_name, labthing_instance=None):
if not labthing_instance: if not labthing_instance:
labthing_instance = _current_labthing() labthing_instance = current_labthing()
if device_name in labthing_instance.devices: if device_name in labthing_instance.devices:
return labthing_instance.devices[device_name] return labthing_instance.devices[device_name]
else: else:
return None return None
def find_plugin(plugin_name, labthing_instance=None): def find_plugin(plugin_name, labthing_instance=None):
if not labthing_instance: if not labthing_instance:
labthing_instance = _current_labthing() labthing_instance = current_labthing()
logging.debug("Current labthing:") logging.debug("Current labthing:")
logging.debug(_current_labthing()) logging.debug(current_labthing())
if plugin_name in labthing_instance.plugins: if plugin_name in labthing_instance.plugins:
return labthing_instance.plugins[plugin_name] 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 .plugins import BasePlugin
from .views.plugins import PluginListResource from .views.plugins import PluginListResource
from .resource import Resource
from ..utilities import get_docstring
from . import EXTENSION_NAME from . import EXTENSION_NAME
class LabThing(object): class LabThing(object):
def __init__(self, app=None, prefix="", description=""): def __init__(self, app=None, prefix="", title="", description=""):
self.app = app self.app = app
self.devices = {} self.devices = {}
@ -15,10 +19,14 @@ class LabThing(object):
self.plugins = {} self.plugins = {}
self.resources = [] self.resources = []
self.properties = {}
self.actions = {}
self.endpoints = set() self.endpoints = set()
self.url_prefix = prefix self.url_prefix = prefix
self.description = description self.description = description
self.title = title
if app is not None: if app is not None:
self.init_app(app) self.init_app(app)
@ -33,10 +41,17 @@ class LabThing(object):
self._create_base_routes() 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): def teardown(self, exception):
print(f"Tearing down devices: {self.devices}") print(f"Tearing down devices: {self.devices}")
def _create_base_routes(self): 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") self.add_resource(PluginListResource, "/plugins")
### Device stuff ### Device stuff
@ -51,8 +66,6 @@ class LabThing(object):
else: else:
raise TypeError("Plugin object must be an instance of BasePlugin") 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(): for plugin_view_id, plugin_view in plugin_object.views.items():
# Add route to the plugins blueprint # Add route to the plugins blueprint
self.add_resource( self.add_resource(
@ -61,6 +74,12 @@ class LabThing(object):
**plugin_view["kwargs"], **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 ### Resource stuff
def _complete_url(self, url_part, registration_prefix): def _complete_url(self, url_part, registration_prefix):
@ -73,7 +92,23 @@ class LabThing(object):
parts = [registration_prefix, self.url_prefix, url_part] parts = [registration_prefix, self.url_prefix, url_part]
return "".join([part for part in parts if 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. """Adds a resource to the api.
:param resource: the class name of your resource :param resource: the class name of your resource
:type resource: :class:`Type[Resource]` :type resource: :class:`Type[Resource]`
@ -81,7 +116,7 @@ class LabThing(object):
flask routing rules apply. Any url variables will be flask routing rules apply. Any url variables will be
passed to the resource method as args. passed to the resource method as args.
:type urls: str :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 Can be used to reference this route in :class:`fields.Url` fields
:type endpoint: str :type endpoint: str
:param resource_class_args: args to be forwarded to the constructor of :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(Foo, '/foo', endpoint="foo")
api.add_resource(FooSpecial, '/special/foo', endpoint="foo") api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
""" """
endpoint = endpoint or resource.__name__
if self.app is not None: 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: else:
self.resources.append((resource, urls, kwargs)) self.resources.append((resource, urls, endpoint, kwargs))
def resource(self, *urls, **kwargs): def resource(self, *urls, **kwargs):
"""Wraps a :class:`~flask_restful.Resource` class, adding it to the """Wraps a :class:`~flask_restful.Resource` class, adding it to the
@ -120,8 +156,8 @@ class LabThing(object):
return decorator return decorator
def _register_view(self, app, resource, *urls, **kwargs): def _register_view(self, app, resource, *urls, endpoint=None, **kwargs):
endpoint = kwargs.pop("endpoint", None) or resource.__name__.lower() endpoint = endpoint or resource.__name__
self.endpoints.add(endpoint) self.endpoints.add(endpoint)
resource_class_args = kwargs.pop("resource_class_args", ()) resource_class_args = kwargs.pop("resource_class_args", ())
resource_class_kwargs = kwargs.pop("resource_class_kwargs", {}) resource_class_kwargs = kwargs.pop("resource_class_kwargs", {})
@ -147,3 +183,36 @@ class LabThing(object):
rule = self._complete_url(url, "") rule = self._complete_url(url, "")
# Add the url to the application or blueprint # Add the url to the application or blueprint
app.add_url_rule(rule, view_func=resource_func, **kwargs) 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._rules = {} # Key: Original rule. Val: View class
self._gui = None self._gui = None
self.actions = []
self.properties = []
self.name = name self.name = name
self.methods = {} self.methods = {}
@ -35,7 +38,7 @@ class BasePlugin:
def views(self): def views(self):
return self._views 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 # Remove all leading slashes from view route
cleaned_rule = rule cleaned_rule = rule
while cleaned_rule[0] == "/": while cleaned_rule[0] == "/":
@ -57,6 +60,12 @@ class BasePlugin:
# Store the rule expansion information # Store the rule expansion information
self._rules[rule] = self._views[view_id] 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 @property
def gui(self): def gui(self):
print(self._gui) print(self._gui)
@ -104,13 +113,16 @@ class BasePlugin:
def _name_uri_safe(self): def _name_uri_safe(self):
return snake_to_spine(self._name_python_safe) 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 self.methods[method_name] = method
if not hasattr(self, method_name): if not hasattr(self, method_name):
setattr(self, method_name, method) setattr(self, method_name, method)
else: 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"): def find_plugins(plugin_path, module_name="plugins"):
print(f"Loading plugins from {plugin_path}") print(f"Loading plugins from {plugin_path}")

View file

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

View file

@ -6,12 +6,21 @@ from .thread import TaskThread
from flask import copy_current_request_context from flask import copy_current_request_context
class TaskMaster: class TaskMaster:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._tasks = [] self._tasks = []
@property @property
def tasks(self): def tasks(self):
"""
Returns:
list: List of TaskThread objects.
"""
return self._tasks
@property
def dict(self):
""" """
Returns: Returns:
dict: Dictionary of TaskThread objects. Key is TaskThread ID. dict: Dictionary of TaskThread objects. Key is TaskThread ID.
@ -28,7 +37,9 @@ class TaskMaster:
def new(self, f, *args, **kwargs): def new(self, f, *args, **kwargs):
# copy_current_request_context allows threads to access flask current_app # 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) self._tasks.append(task)
return task return task
@ -47,13 +58,23 @@ class TaskMaster:
def tasks(): 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 Dictionary of tasks in default taskmaster
Returns: Returns:
dict: Dictionary of tasks in default taskmaster dict: Dictionary of tasks in default taskmaster
""" """
global _default_task_master global _default_task_master
return _default_task_master.tasks return _default_task_master.dict
def states(): 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: else:
return False return False
# Create unified status # Create unified status
@property @property
def status(self): def status(self):
@ -239,7 +238,7 @@ class Microscope:
""" """
system_metadata = { system_metadata = {
"microscope_settings": self.read_settings(), "microscope_settings": self.read_settings(),
"microscope_state": self.state, "microscope_state": self.status,
"microscope_id": self.id, "microscope_id": self.id,
"microscope_name": self.name, "microscope_name": self.name,
} }

View file

@ -356,9 +356,10 @@ class FastAutofocusAPI(MethodView):
autofocus_plugin_v2 = BasePlugin("autofocus") autofocus_plugin_v2 = BasePlugin("autofocus")
autofocus_plugin_v2.add_method("fast_autofocus", fast_autofocus) autofocus_plugin_v2.add_method(fast_autofocus, "fast_autofocus")
autofocus_plugin_v2.add_method("autofocus", 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 ### Grid construction
def construct_grid(initial, step_sizes, n_steps, style="raster"): def construct_grid(initial, step_sizes, n_steps, style="raster"):
""" """
Given an initial position, step sizes, and number of steps, 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_to_be_captured: int = 1
_images_captured_so_far: int = 0 _images_captured_so_far: int = 0
def progress(): def progress():
progress = (_images_captured_so_far / _images_to_be_captured) * 100 progress = (_images_captured_so_far / _images_to_be_captured) * 100
logging.info(progress) logging.info(progress)
@ -72,6 +74,7 @@ def progress():
### Capturing ### Capturing
def capture( def capture(
microscope, microscope,
basename, basename,
@ -114,6 +117,7 @@ def capture(
### Scanning ### Scanning
def tile( def tile(
microscope, microscope,
basename: str = None, basename: str = None,
@ -181,9 +185,7 @@ def tile(
) # shorthand for Z stack range ) # shorthand for Z stack range
# Construct an x-y grid (worry about z later) # Construct an x-y grid (worry about z later)
x_y_grid = construct_grid( x_y_grid = construct_grid(initial_position, step_size[:2], grid[:2], style=style)
initial_position, step_size[:2], grid[:2], style=style
)
# Keep the initial Z position the same as our current position # Keep the initial Z position the same as our current position
next_z = initial_position[2] next_z = initial_position[2]
@ -270,6 +272,7 @@ def tile(
logging.debug("Returning to {}".format(initial_position)) logging.debug("Returning to {}".format(initial_position))
microscope.stage.move_abs(initial_position) microscope.stage.move_abs(initial_position)
def stack( def stack(
microscope, microscope,
basename: str = None, basename: str = None,
@ -336,6 +339,7 @@ def stack(
### Web views ### Web views
class TileScanAPI(MethodView): class TileScanAPI(MethodView):
def post(self): def post(self):
payload = JsonResponse(request) payload = JsonResponse(request)
@ -396,4 +400,5 @@ class TileScanAPI(MethodView):
scan_plugin_v2 = BasePlugin("scan") 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.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1" 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]] [[package]]
category = "dev" category = "dev"
description = "McCabe checker, plugin for flake8" description = "McCabe checker, plugin for flake8"
@ -230,6 +238,7 @@ version = "1.13.1b0"
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb" reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
type = "git" type = "git"
url = "https://github.com/rwb27/picamera.git" url = "https://github.com/rwb27/picamera.git"
[[package]] [[package]]
category = "main" category = "main"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (Fork)"
@ -473,7 +482,7 @@ version = "1.11.2"
rpi = ["picamera", "RPi.GPIO"] rpi = ["picamera", "RPi.GPIO"]
[metadata] [metadata]
content-hash = "ea3fcd1909a76b04efa8ebefdcb77f0b449642820a94429fbbbf20bb4f9a64ab" content-hash = "254e455bd1b5a5482f296f8afccf5bd10d109206e8350b9e8ec304576e0d0ae5"
python-versions = "^3.6" python-versions = "^3.6"
[metadata.hashes] [metadata.hashes]
@ -497,6 +506,7 @@ itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f1
jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] 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"] 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"] 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"] 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"] 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"] packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"]
@ -522,7 +532,7 @@ sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c
sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"] sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"]
sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"] sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] 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"] urllib3 = ["a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", "f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"]
werkzeug = ["7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", "e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"] werkzeug = ["7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", "e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"]
wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] 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. 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 } "RPi.GPIO" = { version = "^0.6.5", optional = true }
marshmallow = "^3.3"
[tool.poetry.extras] [tool.poetry.extras]
rpi = ["picamera", "RPi.GPIO"] rpi = ["picamera", "RPi.GPIO"]