Restructured labthings
This commit is contained in:
parent
206dd521ec
commit
a8a3cafa97
39 changed files with 275 additions and 269 deletions
|
|
@ -2,7 +2,7 @@ from openflexure_microscope.plugins import MicroscopePlugin
|
||||||
from openflexure_microscope.api.views import MicroscopeViewPlugin
|
from openflexure_microscope.api.views import MicroscopeViewPlugin
|
||||||
from openflexure_microscope.api.utilities import JsonResponse
|
from openflexure_microscope.api.utilities import JsonResponse
|
||||||
|
|
||||||
from openflexure_microscope.common.tasks import taskify
|
from openflexure_microscope.common.labthings_core.tasks import taskify
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
|
||||||
|
|
@ -3,29 +3,24 @@
|
||||||
import time
|
import time
|
||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import Flask, jsonify, send_file, url_for
|
from flask import Flask, jsonify, send_file
|
||||||
|
|
||||||
from serial import SerialException
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
from openflexure_microscope.api.exceptions import JSONExceptionHandler
|
from openflexure_microscope.api.utilities import list_routes, init_default_plugins
|
||||||
from openflexure_microscope.api.utilities import list_routes
|
|
||||||
|
|
||||||
from openflexure_microscope.config import (
|
from openflexure_microscope.config import (
|
||||||
settings_file_path,
|
settings_file_path,
|
||||||
JSONEncoder,
|
JSONEncoder,
|
||||||
USER_PLUGINS_PATH,
|
USER_PLUGINS_PATH,
|
||||||
)
|
)
|
||||||
from openflexure_microscope.api import v2
|
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings.labthing import LabThing
|
from openflexure_microscope.common.flask_labthings.labthing import LabThing
|
||||||
from openflexure_microscope.common.labthings.find import registered_plugins
|
from openflexure_microscope.common.flask_labthings.plugins import find_plugins
|
||||||
from openflexure_microscope.common.labthings.plugins import find_plugins
|
|
||||||
|
|
||||||
from openflexure_microscope.api.microscope import default_microscope as api_microscope
|
from openflexure_microscope.api.microscope import default_microscope as api_microscope
|
||||||
|
|
||||||
|
|
@ -36,14 +31,13 @@ is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
||||||
|
|
||||||
DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log")
|
DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log")
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
if (__name__ == "__main__") or (not is_gunicorn):
|
if (__name__ == "__main__") or (not is_gunicorn):
|
||||||
# If imported, but not by gunicorn
|
# If imported, but not by gunicorn
|
||||||
print("Letting sys handle logs")
|
print("Letting sys handle logs")
|
||||||
logger = logging.getLogger()
|
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
# Direct standard Python logging to file and console
|
# Direct standard Python logging to file and console
|
||||||
root = logging.getLogger()
|
|
||||||
error_formatter = logging.Formatter(
|
error_formatter = logging.Formatter(
|
||||||
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
|
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
|
||||||
)
|
)
|
||||||
|
|
@ -56,9 +50,9 @@ else:
|
||||||
|
|
||||||
for handler in error_handlers:
|
for handler in error_handlers:
|
||||||
handler.setFormatter(error_formatter)
|
handler.setFormatter(error_formatter)
|
||||||
root.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
root.setLevel(logging.getLogger("gunicorn.error").level)
|
logger.setLevel(logging.getLogger("gunicorn.error").level)
|
||||||
|
|
||||||
|
|
||||||
# Create flask app
|
# Create flask app
|
||||||
|
|
@ -71,9 +65,6 @@ app.json_encoder = JSONEncoder
|
||||||
# Enable CORS everywhere
|
# Enable CORS everywhere
|
||||||
CORS(app, resources=r"*")
|
CORS(app, resources=r"*")
|
||||||
|
|
||||||
# Make errors more API friendly
|
|
||||||
handler = JSONExceptionHandler(app)
|
|
||||||
|
|
||||||
# Build a labthing
|
# Build a labthing
|
||||||
labthing = LabThing(app, prefix="/api/v2")
|
labthing = LabThing(app, prefix="/api/v2")
|
||||||
labthing.description = "Test LabThing-based API for OpenFlexure Microscope"
|
labthing.description = "Test LabThing-based API for OpenFlexure Microscope"
|
||||||
|
|
@ -83,6 +74,8 @@ labthing.title = f"OpenFlexure Microscope {api_microscope.name}"
|
||||||
labthing.register_device(api_microscope, "openflexure_microscope")
|
labthing.register_device(api_microscope, "openflexure_microscope")
|
||||||
|
|
||||||
# Attach plugins
|
# Attach plugins
|
||||||
|
if not os.path.isfile(USER_PLUGINS_PATH):
|
||||||
|
init_default_plugins(USER_PLUGINS_PATH)
|
||||||
for plugin in find_plugins(USER_PLUGINS_PATH):
|
for plugin in find_plugins(USER_PLUGINS_PATH):
|
||||||
labthing.register_plugin(plugin)
|
labthing.register_plugin(plugin)
|
||||||
|
|
||||||
|
|
@ -120,7 +113,7 @@ labthing.register_property(views.SnapshotStream)
|
||||||
for name, action in views.enabled_root_actions().items():
|
for name, action in views.enabled_root_actions().items():
|
||||||
view_class = action["view_class"]
|
view_class = action["view_class"]
|
||||||
rule = action["rule"]
|
rule = action["rule"]
|
||||||
labthing.add_resource(view_class, "/actions{rule}")
|
labthing.add_resource(view_class, f"/actions{rule}")
|
||||||
labthing.register_action(view_class)
|
labthing.register_action(view_class)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -159,7 +152,6 @@ def err_log():
|
||||||
|
|
||||||
# Automatically clean up microscope at exit
|
# Automatically clean up microscope at exit
|
||||||
def cleanup():
|
def cleanup():
|
||||||
global api_microscope
|
|
||||||
logging.debug("App teardown started...")
|
logging.debug("App teardown started...")
|
||||||
logging.debug("Settling...")
|
logging.debug("Settling...")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,8 @@ except Exception as e:
|
||||||
|
|
||||||
# Initialise stage
|
# Initialise stage
|
||||||
logging.debug("Creating stage object...")
|
logging.debug("Creating stage object...")
|
||||||
try:
|
|
||||||
api_stage = SangaStage()
|
api_stage = MockStage()
|
||||||
except Exception as e:
|
|
||||||
logging.error(e)
|
|
||||||
logging.warning("No valid stage hardware found. Falling back to mock stage!")
|
|
||||||
api_stage = MockStage()
|
|
||||||
|
|
||||||
# Attach devices to microscope
|
# Attach devices to microscope
|
||||||
logging.debug("Attaching devices to microscope...")
|
logging.debug("Attaching devices to microscope...")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import errno
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from flask import url_for, Blueprint
|
from flask import url_for, Blueprint
|
||||||
|
|
||||||
|
|
@ -95,3 +97,31 @@ def list_routes(app):
|
||||||
output[url] = line
|
output[url] = line
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def create_file(config_path):
|
||||||
|
if not os.path.exists(os.path.dirname(config_path)):
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(config_path))
|
||||||
|
except OSError as exc: # Guard against race condition
|
||||||
|
if exc.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def init_default_plugins(plugin_path):
|
||||||
|
global _DEFAULT_PLUGIN_INIT
|
||||||
|
os.makedirs(os.path.dirname(plugin_path), exist_ok=True)
|
||||||
|
|
||||||
|
if not os.path.exists(plugin_path): # If user plugins file doesn't exist
|
||||||
|
logging.warning("No plugin file found at {}. Creating...".format(plugin_path))
|
||||||
|
create_file(plugin_path)
|
||||||
|
|
||||||
|
logging.info("Populating {}...".format(plugin_path))
|
||||||
|
with open(plugin_path, "w") as outfile:
|
||||||
|
outfile.write(_DEFAULT_PLUGIN_INIT)
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_PLUGIN_INIT = """from openflexure_microscope.plugins.v2.autofocus import autofocus_plugin_v2
|
||||||
|
from openflexure_microscope.plugins.v2.scan import scan_plugin_v2
|
||||||
|
|
||||||
|
__plugins__ = [autofocus_plugin_v2, scan_plugin_v2]"""
|
||||||
|
|
@ -2,11 +2,6 @@
|
||||||
Top-level representation of enabled actions
|
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 . import camera, stage, system
|
from . import camera, stage, system
|
||||||
|
|
||||||
_actions = {
|
_actions = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
from openflexure_microscope.common.labthings.find import find_device
|
from openflexure_microscope.common.flask_labthings.find import find_device
|
||||||
from openflexure_microscope.utilities import filter_dict
|
from openflexure_microscope.utilities import filter_dict
|
||||||
|
|
||||||
from openflexure_microscope.api.v2.views.captures import capture_schema
|
from openflexure_microscope.api.v2.views.captures import capture_schema
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from openflexure_microscope.api.utilities import JsonResponse
|
from openflexure_microscope.api.utilities import JsonResponse
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
from openflexure_microscope.common.labthings.find import find_device
|
from openflexure_microscope.common.flask_labthings.find import find_device
|
||||||
from openflexure_microscope.utilities import axes_to_array, filter_dict
|
from openflexure_microscope.utilities import axes_to_array, filter_dict
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
from sys import platform
|
from sys import platform
|
||||||
|
|
|
||||||
|
|
@ -3,47 +3,12 @@ from flask import abort, request, redirect, url_for, send_file, jsonify
|
||||||
|
|
||||||
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings.schema import Schema
|
from openflexure_microscope.common.flask_labthings.schema import Schema
|
||||||
from openflexure_microscope.common.labthings import fields
|
from openflexure_microscope.common.flask_labthings import fields
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
|
from openflexure_microscope.common.flask_labthings.utilities import description_from_view
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings.find import find_device
|
from openflexure_microscope.common.flask_labthings.find import find_device
|
||||||
|
|
||||||
|
|
||||||
class CaptureSchema(Schema):
|
|
||||||
id = fields.String()
|
|
||||||
file = fields.String(data_key="path")
|
|
||||||
exists = fields.Bool(data_key="available")
|
|
||||||
filename = fields.String()
|
|
||||||
metadata = fields.Dict()
|
|
||||||
|
|
||||||
# TODO: Add HTTP methods
|
|
||||||
links = fields.Hyperlinks(
|
|
||||||
{
|
|
||||||
"self": {
|
|
||||||
"href": fields.AbsoluteUrlFor("CaptureResource", id="<id>"),
|
|
||||||
"mimetype": "application/json",
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"href": fields.AbsoluteUrlFor("CaptureTags", id="<id>"),
|
|
||||||
"mimetype": "application/json",
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"href": fields.AbsoluteUrlFor("CaptureMetadata", id="<id>"),
|
|
||||||
"mimetype": "application/json",
|
|
||||||
},
|
|
||||||
"download": {
|
|
||||||
"href": fields.AbsoluteUrlFor(
|
|
||||||
"CaptureDownload", id="<id>", filename="<filename>"
|
|
||||||
),
|
|
||||||
"mimetype": "image/jpeg",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
capture_schema = CaptureSchema()
|
|
||||||
capture_list_schema = CaptureSchema(many=True)
|
|
||||||
|
|
||||||
|
|
||||||
class CaptureList(Resource):
|
class CaptureList(Resource):
|
||||||
|
|
@ -182,6 +147,46 @@ class CaptureMetadata(Resource):
|
||||||
return jsonify(capture_obj.metadata)
|
return jsonify(capture_obj.metadata)
|
||||||
|
|
||||||
|
|
||||||
|
class CaptureSchema(Schema):
|
||||||
|
id = fields.String()
|
||||||
|
file = fields.String(data_key="path")
|
||||||
|
exists = fields.Bool(data_key="available")
|
||||||
|
filename = fields.String()
|
||||||
|
metadata = fields.Dict()
|
||||||
|
|
||||||
|
# TODO: Add HTTP methods
|
||||||
|
links = fields.Hyperlinks(
|
||||||
|
{
|
||||||
|
"self": {
|
||||||
|
"href": fields.AbsoluteUrlFor(CaptureResource, id="<id>"),
|
||||||
|
"mimetype": "application/json",
|
||||||
|
**description_from_view(CaptureResource)
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"href": fields.AbsoluteUrlFor(CaptureTags, id="<id>"),
|
||||||
|
"mimetype": "application/json",
|
||||||
|
**description_from_view(CaptureTags)
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"href": fields.AbsoluteUrlFor(CaptureMetadata, id="<id>"),
|
||||||
|
"mimetype": "application/json",
|
||||||
|
**description_from_view(CaptureMetadata)
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"href": fields.AbsoluteUrlFor(
|
||||||
|
CaptureDownload, id="<id>", filename="<filename>"
|
||||||
|
),
|
||||||
|
"mimetype": "image/jpeg",
|
||||||
|
**description_from_view(CaptureDownload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
capture_schema = CaptureSchema()
|
||||||
|
capture_list_schema = CaptureSchema(many=True)
|
||||||
|
|
||||||
|
|
||||||
def add_captures_to_labthing(labthing, prefix=""):
|
def add_captures_to_labthing(labthing, prefix=""):
|
||||||
"""
|
"""
|
||||||
Add all capture resources to a labthing
|
Add all capture resources to a labthing
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
from openflexure_microscope.api.utilities import JsonResponse
|
from openflexure_microscope.api.utilities import JsonResponse
|
||||||
from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path
|
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.flask_labthings.find import find_device
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
|
|
||||||
from flask import jsonify, request, abort
|
from flask import jsonify, request, abort
|
||||||
import logging
|
import logging
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
from openflexure_microscope.api.utilities import gen, JsonResponse
|
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.utilities import get_by_path, set_by_path, create_from_path
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings.find import find_device
|
from openflexure_microscope.common.flask_labthings.find import find_device
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
|
|
||||||
from flask import jsonify, request, abort, Response
|
from flask import jsonify, request, abort, Response
|
||||||
import logging
|
import logging
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from .capture import CaptureObject
|
from .capture import CaptureObject
|
||||||
from openflexure_microscope.utilities import entry_by_id
|
from openflexure_microscope.utilities import entry_by_id
|
||||||
from openflexure_microscope.common.lock import StrictLock
|
from openflexure_microscope.common.labthings_core.lock import StrictLock
|
||||||
|
|
||||||
|
|
||||||
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs")
|
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs")
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
from . import tasks, labthings
|
from . import flask_labthings
|
||||||
|
from openflexure_microscope.common.labthings_core import tasks
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class JSONExceptionHandler(object):
|
||||||
|
|
||||||
status_code = error.code if isinstance(error, HTTPException) else 500
|
status_code = error.code if isinstance(error, HTTPException) else 500
|
||||||
|
|
||||||
response = {"status_code": status_code, "message": escape(message)}
|
response = {"code": status_code, "message": escape(message)}
|
||||||
return jsonify(response), status_code
|
return jsonify(response), status_code
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
|
|
@ -2,6 +2,7 @@ from marshmallow.fields import *
|
||||||
from marshmallow import missing
|
from marshmallow import missing
|
||||||
import re
|
import re
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
from flask.views import View
|
||||||
|
|
||||||
_tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*")
|
_tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*")
|
||||||
|
|
||||||
|
|
@ -62,7 +63,16 @@ class URLFor(Field):
|
||||||
_CHECK_ATTRIBUTE = False
|
_CHECK_ATTRIBUTE = False
|
||||||
|
|
||||||
def __init__(self, endpoint, **kwargs):
|
def __init__(self, endpoint, **kwargs):
|
||||||
self.endpoint = endpoint
|
# Handle the case where endpoint is an attached flask View of any kind
|
||||||
|
if isinstance(endpoint, type) and issubclass(endpoint, View):
|
||||||
|
self.view_class = endpoint
|
||||||
|
self.endpoint = None
|
||||||
|
# Handle cases where endpoint is passed directly as a string
|
||||||
|
elif type(endpoint) == str:
|
||||||
|
self.view_class = None
|
||||||
|
self.endpoint = endpoint
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Endpoint {endpoint} is not a valid Flask view or endpoint string.")
|
||||||
self.params = kwargs
|
self.params = kwargs
|
||||||
Field.__init__(self, **kwargs)
|
Field.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
|
@ -70,6 +80,14 @@ class URLFor(Field):
|
||||||
"""Output the URL for the endpoint, given the kwargs passed to
|
"""Output the URL for the endpoint, given the kwargs passed to
|
||||||
``__init__``.
|
``__init__``.
|
||||||
"""
|
"""
|
||||||
|
# Get endpoint from view_class, if needed
|
||||||
|
if self.view_class and not self.endpoint:
|
||||||
|
if hasattr(self.view_class, "endpoint"):
|
||||||
|
self.endpoint = self.view_class.endpoint
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Resource {self.endpoint} has not been added to a LabThing application. Unable to generate URL.")
|
||||||
|
|
||||||
|
# Generate URL for
|
||||||
param_values = {}
|
param_values = {}
|
||||||
for name, attr_tpl in self.params.items():
|
for name, attr_tpl in self.params.items():
|
||||||
attr_name = _tpl(str(attr_tpl))
|
attr_name = _tpl(str(attr_tpl))
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
from flask import current_app, _app_ctx_stack, request, url_for, jsonify
|
from flask import url_for, jsonify
|
||||||
|
|
||||||
from .plugins import BasePlugin
|
from .plugins import BasePlugin
|
||||||
from .views.plugins import PluginListResource
|
from .views.plugins import PluginListResource
|
||||||
from .views.tasks import TaskList, TaskResource
|
from .views.tasks import TaskList, TaskResource
|
||||||
|
|
||||||
from ..utilities import get_docstring
|
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
||||||
|
from .exceptions import JSONExceptionHandler
|
||||||
|
|
||||||
from . import EXTENSION_NAME
|
from . import EXTENSION_NAME
|
||||||
|
|
||||||
|
|
||||||
class LabThing(object):
|
class LabThing(object):
|
||||||
def __init__(self, app=None, prefix="", title="", description=""):
|
def __init__(self, app=None, prefix: str = "", title: str = "", description: str = "", handle_errors: bool = True):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
self.devices = {}
|
self.devices = {}
|
||||||
|
|
@ -27,6 +28,11 @@ class LabThing(object):
|
||||||
self.description = description
|
self.description = description
|
||||||
self.title = title
|
self.title = title
|
||||||
|
|
||||||
|
if handle_errors:
|
||||||
|
self.error_handler = JSONExceptionHandler()
|
||||||
|
else:
|
||||||
|
self.error_handler = None
|
||||||
|
|
||||||
if app is not None:
|
if app is not None:
|
||||||
self.init_app(app)
|
self.init_app(app)
|
||||||
|
|
||||||
|
|
@ -35,21 +41,28 @@ class LabThing(object):
|
||||||
def init_app(self, app):
|
def init_app(self, app):
|
||||||
app.teardown_appcontext(self.teardown)
|
app.teardown_appcontext(self.teardown)
|
||||||
|
|
||||||
|
# Register Flask extension
|
||||||
app.extensions = getattr(app, "extensions", {})
|
app.extensions = getattr(app, "extensions", {})
|
||||||
app.extensions[EXTENSION_NAME] = self
|
app.extensions[EXTENSION_NAME] = self
|
||||||
|
|
||||||
|
# Register error handler if one exists
|
||||||
|
if self.error_handler:
|
||||||
|
self.error_handler.init_app(self.app)
|
||||||
|
|
||||||
|
# Create base routes
|
||||||
self._create_base_routes()
|
self._create_base_routes()
|
||||||
|
|
||||||
|
# Add resources, if registered before tying to a Flask app
|
||||||
if len(self.resources) > 0:
|
if len(self.resources) > 0:
|
||||||
for resource, urls, endpoint, kwargs in self.resources:
|
for resource, urls, endpoint, kwargs in self.resources:
|
||||||
self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs)
|
self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs)
|
||||||
|
|
||||||
def teardown(self, exception):
|
def teardown(self, exception):
|
||||||
print(f"Tearing down devices: {self.devices}")
|
pass
|
||||||
|
|
||||||
def _create_base_routes(self):
|
def _create_base_routes(self):
|
||||||
# Add thing description to root
|
# Add thing description to root
|
||||||
self.app.add_url_rule(self._complete_url("/", ""), "td", self.td)
|
self.app.add_url_rule(self._complete_url("/td", ""), "td", self.td)
|
||||||
# Add plugin overview
|
# Add plugin overview
|
||||||
self.add_resource(PluginListResource, "/plugins")
|
self.add_resource(PluginListResource, "/plugins")
|
||||||
self.register_property(PluginListResource)
|
self.register_property(PluginListResource)
|
||||||
|
|
@ -64,6 +77,7 @@ class LabThing(object):
|
||||||
self.devices[device_name] = device_object
|
self.devices[device_name] = device_object
|
||||||
|
|
||||||
### Plugin stuff
|
### Plugin stuff
|
||||||
|
|
||||||
def register_plugin(self, plugin_object):
|
def register_plugin(self, plugin_object):
|
||||||
if isinstance(plugin_object, BasePlugin):
|
if isinstance(plugin_object, BasePlugin):
|
||||||
self.plugins[plugin_object.name] = plugin_object
|
self.plugins[plugin_object.name] = plugin_object
|
||||||
|
|
@ -196,8 +210,25 @@ class LabThing(object):
|
||||||
endpoint = resource.endpoint
|
endpoint = resource.endpoint
|
||||||
return url_for(endpoint, **values)
|
return url_for(endpoint, **values)
|
||||||
|
|
||||||
|
def owns_endpoint(self, endpoint):
|
||||||
|
"""Tests if an endpoint name (not path) belongs to this Api. Takes
|
||||||
|
in to account the Blueprint name part of the endpoint name.
|
||||||
|
:param endpoint: The name of the endpoint being checked
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.blueprint:
|
||||||
|
if endpoint.startswith(self.blueprint.name):
|
||||||
|
endpoint = endpoint.split(self.blueprint.name + '.', 1)[-1]
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return endpoint in self.endpoints
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
def td(self):
|
def td(self):
|
||||||
|
"""
|
||||||
|
W3C-style Thing Description
|
||||||
|
"""
|
||||||
props = {}
|
props = {}
|
||||||
for key, prop in self.properties.items():
|
for key, prop in self.properties.items():
|
||||||
props[key] = {}
|
props[key] = {}
|
||||||
|
|
@ -221,3 +252,5 @@ class LabThing(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonify(td)
|
return jsonify(td)
|
||||||
|
|
||||||
|
# TODO: Add a nicer root resource like the old self-documenting system
|
||||||
|
|
@ -5,10 +5,8 @@ import copy
|
||||||
from importlib import util
|
from importlib import util
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from openflexure_microscope.config import USER_CONFIG_DIR
|
|
||||||
from openflexure_microscope.utilities import (
|
from openflexure_microscope.utilities import (
|
||||||
camel_to_snake,
|
camel_to_snake,
|
||||||
camel_to_spine,
|
|
||||||
snake_to_spine,
|
snake_to_spine,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -125,11 +123,6 @@ class BasePlugin:
|
||||||
|
|
||||||
|
|
||||||
def find_plugins(plugin_path, module_name="plugins"):
|
def find_plugins(plugin_path, module_name="plugins"):
|
||||||
print(f"Loading plugins from {plugin_path}")
|
|
||||||
# plugin_path = os.path.join(USER_CONFIG_DIR, "microscope_plugins")
|
|
||||||
# plugins = importlib.machinery.SourceFileLoader(
|
|
||||||
# module_name, plugin_path
|
|
||||||
# ).exec_module()
|
|
||||||
logging.debug(f"Loading plugins from {plugin_path}")
|
logging.debug(f"Loading plugins from {plugin_path}")
|
||||||
|
|
||||||
spec = util.spec_from_file_location(module_name, plugin_path)
|
spec = util.spec_from_file_location(module_name, plugin_path)
|
||||||
13
openflexure_microscope/common/flask_labthings/utilities.py
Normal file
13
openflexure_microscope/common/flask_labthings/utilities.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
||||||
|
|
||||||
|
|
||||||
|
def description_from_view(view_class):
|
||||||
|
methods = []
|
||||||
|
for method_key in ["get", "post", "put", "delete"]:
|
||||||
|
if hasattr(view_class, method_key):
|
||||||
|
methods.append(method_key.upper())
|
||||||
|
brief_description = get_docstring(view_class).partition("\n")[0].strip()
|
||||||
|
|
||||||
|
d = {"methods": methods, "description": brief_description}
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""
|
||||||
|
Top-level representation of attached and enabled plugins
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openflexure_microscope.common.labthings_core.utilities import get_docstring
|
||||||
|
from ..utilities import description_from_view
|
||||||
|
|
||||||
|
from openflexure_microscope.common.flask_labthings.find import registered_plugins
|
||||||
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
|
|
||||||
|
from flask import jsonify, url_for
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def plugins_representation(plugin_dict):
|
||||||
|
"""
|
||||||
|
Generate a dictionary representation of all plugins, including Flask route URLs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_dict (dict): Dictionary of plugin objects
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary representation of all plugins
|
||||||
|
"""
|
||||||
|
plugins = {}
|
||||||
|
|
||||||
|
for plugin_name, plugin in plugin_dict.items():
|
||||||
|
logging.debug(f"Representing plugin {plugin._name}")
|
||||||
|
d = {
|
||||||
|
"python_name": plugin._name_python_safe,
|
||||||
|
"plugin": str(plugin),
|
||||||
|
"links": {},
|
||||||
|
"gui": plugin.gui,
|
||||||
|
"description": get_docstring(plugin),
|
||||||
|
}
|
||||||
|
|
||||||
|
for view_id, view_data in plugin.views.items():
|
||||||
|
uri = url_for(f"PluginListResource", _external=True) + "/" + view_data["rule"][1:]
|
||||||
|
# Make links dictionary if it doesn't yet exist
|
||||||
|
view_d = {"href": uri, **description_from_view(view_data["view"])}
|
||||||
|
|
||||||
|
d["links"][view_id] = view_d
|
||||||
|
|
||||||
|
plugins[plugin_name] = d
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
|
class PluginListResource(Resource):
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return jsonify(plugins_representation(registered_plugins()))
|
||||||
|
|
@ -1,38 +1,10 @@
|
||||||
import logging
|
from flask import abort
|
||||||
from flask import abort, request, redirect, url_for, send_file, jsonify
|
|
||||||
|
|
||||||
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
from openflexure_microscope.common.flask_labthings.schema import Schema
|
||||||
|
from openflexure_microscope.common.flask_labthings import fields
|
||||||
from openflexure_microscope.common.labthings.schema import Schema
|
from openflexure_microscope.common.labthings_core import tasks
|
||||||
from openflexure_microscope.common.labthings import fields
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
from openflexure_microscope.common.flask_labthings.utilities import description_from_view
|
||||||
|
|
||||||
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):
|
class TaskList(Resource):
|
||||||
|
|
@ -59,3 +31,29 @@ class TaskResource(Resource):
|
||||||
task.terminate()
|
task.terminate()
|
||||||
|
|
||||||
return task_schema.jsonify(task)
|
return task_schema.jsonify(task)
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
**description_from_view(TaskResource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
task_schema = TaskSchema()
|
||||||
|
task_list_schema = TaskSchema(many=True)
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
"""
|
|
||||||
Top-level representation of attached and enabled plugins
|
|
||||||
"""
|
|
||||||
|
|
||||||
from openflexure_microscope.utilities import get_docstring, description_from_view
|
|
||||||
from openflexure_microscope.api.utilities import blueprint_for_module
|
|
||||||
|
|
||||||
from openflexure_microscope.common.labthings.find import registered_plugins
|
|
||||||
from openflexure_microscope.common.labthings.resource import Resource
|
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, url_for
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import logging
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
|
|
||||||
def plugins_representation(plugin_dict):
|
|
||||||
"""
|
|
||||||
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_name, plugin in plugin_dict.items():
|
|
||||||
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"PluginListResource") + "/" + 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 PluginListResource(Resource):
|
|
||||||
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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return jsonify(plugins_representation(registered_plugins()))
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
def construct_blueprint():
|
|
||||||
|
|
||||||
blueprint = blueprint_for_module(__name__)
|
|
||||||
|
|
||||||
for plugin_obj in registered_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"))
|
|
||||||
|
|
||||||
# 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}", plugin=plugin
|
|
||||||
),
|
|
||||||
**plugin_view["kwargs"],
|
|
||||||
)
|
|
||||||
|
|
||||||
return blueprint
|
|
||||||
"""
|
|
||||||
0
openflexure_microscope/common/labthings_core/__init__.py
Normal file
0
openflexure_microscope/common/labthings_core/__init__.py
Normal file
|
|
@ -8,7 +8,7 @@ as well as some Flask imports to simplify API route development
|
||||||
from openflexure_microscope.api.utilities import JsonResponse
|
from openflexure_microscope.api.utilities import JsonResponse
|
||||||
|
|
||||||
# Task management
|
# Task management
|
||||||
from openflexure_microscope.common.tasks import (
|
from openflexure_microscope.common.labthings_core.tasks import (
|
||||||
current_task,
|
current_task,
|
||||||
update_task_progress,
|
update_task_progress,
|
||||||
update_task_data,
|
update_task_data,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from openflexure_microscope.camera.base import BaseCamera
|
||||||
from openflexure_microscope.camera.mock import MockStreamer
|
from openflexure_microscope.camera.mock import MockStreamer
|
||||||
|
|
||||||
from openflexure_microscope.utilities import serialise_array_b64
|
from openflexure_microscope.utilities import serialise_array_b64
|
||||||
from openflexure_microscope.common.lock import CompositeLock
|
from openflexure_microscope.common.labthings_core.lock import CompositeLock
|
||||||
from openflexure_microscope.config import user_settings
|
from openflexure_microscope.config import user_settings
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,5 @@
|
||||||
4100,
|
4100,
|
||||||
3146
|
3146
|
||||||
],
|
],
|
||||||
"jpeg_quality": 75,
|
"jpeg_quality": 75
|
||||||
"plugins": [
|
|
||||||
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
|
||||||
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
|
||||||
"openflexure_microscope.plugins.default.camera_calibration:AutocalibrationPlugin",
|
|
||||||
"openflexure_microscope.plugins.default.zip_builder:ZipBuilderPlugin"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from openflexure_microscope.common.labthings.find import find_device
|
from openflexure_microscope.common.flask_labthings.find import find_device
|
||||||
from openflexure_microscope.common.labthings.plugins import BasePlugin
|
from openflexure_microscope.common.flask_labthings.plugins import BasePlugin
|
||||||
from openflexure_microscope.microscope import Microscope
|
from openflexure_microscope.microscope import Microscope
|
||||||
|
|
||||||
from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort
|
from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ from typing import Tuple
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from openflexure_microscope.camera.base import generate_basename
|
from openflexure_microscope.camera.base import generate_basename
|
||||||
from openflexure_microscope.common.labthings.find import find_device, find_plugin
|
from openflexure_microscope.common.flask_labthings.find import find_device, find_plugin
|
||||||
from openflexure_microscope.common.labthings.plugins import BasePlugin
|
from openflexure_microscope.common.flask_labthings.plugins import BasePlugin
|
||||||
|
|
||||||
from openflexure_microscope.devel import (
|
from openflexure_microscope.devel import (
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from openflexure_microscope.common.lock import StrictLock
|
from openflexure_microscope.common.labthings_core.lock import StrictLock
|
||||||
|
|
||||||
|
|
||||||
class BaseStage(metaclass=ABCMeta):
|
class BaseStage(metaclass=ABCMeta):
|
||||||
|
|
|
||||||
|
|
@ -41,26 +41,6 @@ def bottom_level_name(obj):
|
||||||
return obj.__name__.split(".")[-1]
|
return obj.__name__.split(".")[-1]
|
||||||
|
|
||||||
|
|
||||||
def description_from_view(view_class):
|
|
||||||
methods = []
|
|
||||||
for method_key in ["get", "post", "put", "delete"]:
|
|
||||||
if hasattr(view_class, method_key):
|
|
||||||
methods.append(method_key.upper())
|
|
||||||
brief_description = get_docstring(view_class).partition("\n")[0].strip()
|
|
||||||
|
|
||||||
d = {"methods": methods, "description": brief_description}
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
def get_docstring(obj):
|
|
||||||
ds = obj.__doc__
|
|
||||||
if ds:
|
|
||||||
return ds.strip()
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def camel_to_snake(name):
|
def camel_to_snake(name):
|
||||||
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
||||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue