First draft of new API plugin system
This commit is contained in:
parent
3a63f7d200
commit
477abb6970
9 changed files with 234 additions and 49 deletions
|
|
@ -16,25 +16,18 @@ from flask_cors import CORS
|
|||
from openflexure_microscope.api.exceptions import JSONExceptionHandler
|
||||
from openflexure_microscope.api.utilities import list_routes
|
||||
|
||||
from openflexure_microscope import Microscope
|
||||
|
||||
from openflexure_microscope.camera.capture import build_captures_from_exif
|
||||
from openflexure_microscope.config import settings_file_path, JSONEncoder
|
||||
from openflexure_microscope.config import (
|
||||
settings_file_path,
|
||||
JSONEncoder,
|
||||
USER_PLUGINS_PATH,
|
||||
)
|
||||
from openflexure_microscope.api.v1 import blueprints
|
||||
from openflexure_microscope.api import v2
|
||||
|
||||
# Import device modules
|
||||
# NB this will eventually be handled by the RC file, so you can choose what device
|
||||
# class should be attached.
|
||||
try:
|
||||
from openflexure_microscope.camera.pi import PiCameraStreamer
|
||||
except ImportError:
|
||||
logging.warning("Unable to import PiCameraStreamer")
|
||||
from openflexure_microscope.camera.mock import MockStreamer
|
||||
|
||||
from openflexure_microscope.stage.sanga import SangaStage
|
||||
from openflexure_microscope.stage.mock import MockStage
|
||||
from openflexure_microscope.common.labthings import LabThing
|
||||
from openflexure_microscope.common.labthings.plugins import find_plugins
|
||||
|
||||
from openflexure_microscope.api.microscope import default_microscope as api_microscope
|
||||
|
||||
# Handle logging
|
||||
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
||||
|
|
@ -54,7 +47,7 @@ else:
|
|||
)
|
||||
|
||||
rotating_logfile = logging.handlers.RotatingFileHandler(
|
||||
DEFAULT_LOGFILE, maxBytes=1000000, backupCount=7
|
||||
DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7
|
||||
)
|
||||
|
||||
error_handlers = [rotating_logfile, logging.StreamHandler()]
|
||||
|
|
@ -65,39 +58,6 @@ else:
|
|||
|
||||
root.setLevel(logging.getLogger("gunicorn.error").level)
|
||||
|
||||
# Create a dummy microscope object, with no hardware attachments
|
||||
api_microscope = Microscope()
|
||||
|
||||
# Initialise camera
|
||||
logging.debug("Creating camera object...")
|
||||
try:
|
||||
api_camera = PiCameraStreamer()
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
logging.warning("No valid camera hardware found. Falling back to mock camera!")
|
||||
api_camera = MockStreamer()
|
||||
|
||||
# Initialise stage
|
||||
logging.debug("Creating stage object...")
|
||||
try:
|
||||
api_stage = SangaStage()
|
||||
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
|
||||
logging.debug("Attaching devices to microscope...")
|
||||
api_microscope.attach(api_camera, api_stage)
|
||||
|
||||
# Restore loaded capture array to camera object
|
||||
logging.debug("Restoring captures...")
|
||||
api_microscope.camera.images = build_captures_from_exif(
|
||||
api_microscope.camera.paths["default"]
|
||||
)
|
||||
|
||||
logging.debug("Microscope successfully attached!")
|
||||
|
||||
|
||||
# Generate API URI based on version from filename
|
||||
def uri(suffix, api_version, base=None):
|
||||
|
|
@ -121,6 +81,10 @@ CORS(app, resources=r"*")
|
|||
# Make errors more API friendly
|
||||
handler = JSONExceptionHandler(app)
|
||||
|
||||
# Attach lab devices
|
||||
labthing = LabThing(app)
|
||||
labthing.register_device(api_microscope, "openflexure_microscope")
|
||||
|
||||
# WEBAPP ROUTES
|
||||
|
||||
# Base routes
|
||||
|
|
@ -176,6 +140,21 @@ v2_actions_blueprint = v2.blueprints.actions.construct_blueprint(api_microscope)
|
|||
app.register_blueprint(v2_actions_blueprint, url_prefix=uri("/actions", "v2"))
|
||||
|
||||
|
||||
plugins = find_plugins(USER_PLUGINS_PATH)
|
||||
print(plugins.__plugins__)
|
||||
|
||||
for plugin_obj in plugins.__plugins__:
|
||||
for plugin_view_id, plugin_view in plugin_obj.views.items():
|
||||
# Add route to the plugins blueprint
|
||||
app.add_url_rule(
|
||||
"/new-plugins" + plugin_view["rule"],
|
||||
view_func=plugin_view["view"].as_view(
|
||||
f"{plugin_obj._name_python_safe}_{plugin_view_id}"
|
||||
),
|
||||
**plugin_view["kwargs"],
|
||||
)
|
||||
|
||||
|
||||
@app.route("/routes")
|
||||
def routes():
|
||||
"""
|
||||
|
|
|
|||
48
openflexure_microscope/api/microscope.py
Normal file
48
openflexure_microscope/api/microscope.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from openflexure_microscope import Microscope
|
||||
from openflexure_microscope.camera.capture import build_captures_from_exif
|
||||
|
||||
import logging
|
||||
|
||||
# Import device modules
|
||||
# NB this will eventually be handled by the RC file, so you can choose what device
|
||||
# class should be attached.
|
||||
try:
|
||||
from openflexure_microscope.camera.pi import PiCameraStreamer
|
||||
except ImportError:
|
||||
logging.warning("Unable to import PiCameraStreamer")
|
||||
from openflexure_microscope.camera.mock import MockStreamer
|
||||
|
||||
from openflexure_microscope.stage.sanga import SangaStage
|
||||
from openflexure_microscope.stage.mock import MockStage
|
||||
|
||||
default_microscope = Microscope()
|
||||
|
||||
# Initialise camera
|
||||
logging.debug("Creating camera object...")
|
||||
try:
|
||||
api_camera = PiCameraStreamer()
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
logging.warning("No valid camera hardware found. Falling back to mock camera!")
|
||||
api_camera = MockStreamer()
|
||||
|
||||
# Initialise stage
|
||||
logging.debug("Creating stage object...")
|
||||
try:
|
||||
api_stage = SangaStage()
|
||||
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
|
||||
logging.debug("Attaching devices to microscope...")
|
||||
default_microscope.attach(api_camera, api_stage)
|
||||
|
||||
# Restore loaded capture array to camera object
|
||||
logging.debug("Restoring captures...")
|
||||
default_microscope.camera.images = build_captures_from_exif(
|
||||
default_microscope.camera.paths["default"]
|
||||
)
|
||||
|
||||
logging.debug("Microscope successfully attached!")
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import tasks, labthings
|
||||
37
openflexure_microscope/common/labthings/__init__.py
Normal file
37
openflexure_microscope/common/labthings/__init__.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from flask import current_app, _app_ctx_stack
|
||||
|
||||
EXTENSION_NAME = "flask-lab"
|
||||
|
||||
|
||||
class LabThing(object):
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
self.devices = {}
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
app.teardown_appcontext(self.teardown)
|
||||
|
||||
app.extensions = getattr(app, "extensions", {})
|
||||
app.extensions[EXTENSION_NAME] = self
|
||||
|
||||
def teardown(self, exception):
|
||||
print(f"Tearing down devices: {self.devices}")
|
||||
|
||||
def register_device(self, device_object, device_name: str):
|
||||
self.devices[device_name] = device_object
|
||||
|
||||
|
||||
def find_device(device_name):
|
||||
app = current_app
|
||||
|
||||
if not app:
|
||||
return None
|
||||
|
||||
pylot_instance = app.extensions[EXTENSION_NAME]
|
||||
|
||||
if device_name in pylot_instance.devices:
|
||||
return pylot_instance.devices[device_name]
|
||||
else:
|
||||
return None
|
||||
0
openflexure_microscope/common/labthings/fields.py
Normal file
0
openflexure_microscope/common/labthings/fields.py
Normal file
119
openflexure_microscope/common/labthings/plugins.py
Normal file
119
openflexure_microscope/common/labthings/plugins.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import logging
|
||||
import collections
|
||||
import copy
|
||||
|
||||
from importlib import util
|
||||
import sys
|
||||
|
||||
from openflexure_microscope.config import USER_CONFIG_DIR
|
||||
from openflexure_microscope.utilities import camel_to_snake, camel_to_spine
|
||||
|
||||
|
||||
class BasePlugin:
|
||||
"""
|
||||
Parent class for all plugins.
|
||||
|
||||
Handles binding route views and forms.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._views = (
|
||||
{}
|
||||
) # Key: Full, Python-safe ID. Val: Original rule, and view class
|
||||
self._rules = {} # Key: Original rule. Val: View class
|
||||
self._gui = None
|
||||
|
||||
@property
|
||||
def views(self):
|
||||
return self._views
|
||||
|
||||
def add_view(self, rule, view_class, **kwargs):
|
||||
# Remove all leading slashes from view route
|
||||
cleaned_rule = rule
|
||||
while cleaned_rule[0] == "/":
|
||||
cleaned_rule = cleaned_rule[1:]
|
||||
|
||||
# Expand the rule to include plugin name
|
||||
full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule)
|
||||
|
||||
view_id = cleaned_rule.replace("/", "_").replace("<", "").replace(">", "")
|
||||
|
||||
# Create a Python-safe route ID
|
||||
logging.debug(view_id)
|
||||
|
||||
# Store route information in a dictionary
|
||||
d = {"rule": full_rule, "view": view_class, "kwargs": kwargs}
|
||||
|
||||
# Add view to private views dictionary
|
||||
self._views[view_id] = d
|
||||
# Store the rule expansion information
|
||||
self._rules[rule] = self._views[view_id]
|
||||
|
||||
@property
|
||||
def gui(self):
|
||||
print(self._gui)
|
||||
# Handle missing/no GUI
|
||||
if not self._gui:
|
||||
return None
|
||||
# Handle GUI as a dictionary-returning callable function
|
||||
elif isinstance(self._gui, collections.Callable):
|
||||
api_gui = self._gui()
|
||||
# Handle GUI as a static dictionary
|
||||
elif isinstance(self._gui, dict):
|
||||
api_gui = copy.deepcopy(self._gui)
|
||||
# Handle borked GUI
|
||||
else:
|
||||
raise ValueError(
|
||||
"GUI must be a dictionary, or a dictionary-returning function with no arguments."
|
||||
)
|
||||
|
||||
api_gui["id"] = self._name
|
||||
|
||||
if "forms" in api_gui and isinstance(api_gui["forms"], list):
|
||||
for form in api_gui["forms"]:
|
||||
if "route" in form and form["route"] in self._rules.keys():
|
||||
form["route"] = self._rules[form["route"]]["rule"]
|
||||
else:
|
||||
logging.warn(
|
||||
"No valid expandable route found for {}".format(form["route"])
|
||||
)
|
||||
return api_gui
|
||||
|
||||
def set_gui(self, form_dictionary: dict):
|
||||
self._gui = form_dictionary
|
||||
|
||||
@property
|
||||
def _name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def _name_python_safe(self):
|
||||
return camel_to_snake(self._name)
|
||||
|
||||
@property
|
||||
def _name_uri_safe(self):
|
||||
return camel_to_spine(self._name)
|
||||
|
||||
def _full_name(self):
|
||||
module = self.__class__.__module__
|
||||
if module is None or module == str.__class__.__module__:
|
||||
return self.__class__.__name__ # Avoid reporting __builtin__
|
||||
else:
|
||||
return module + "." + self.__class__.__name__
|
||||
|
||||
|
||||
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}")
|
||||
|
||||
spec = util.spec_from_file_location(module_name, plugin_path)
|
||||
mod = util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = mod
|
||||
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
return mod
|
||||
0
openflexure_microscope/common/labthings/schema.py
Normal file
0
openflexure_microscope/common/labthings/schema.py
Normal file
0
openflexure_microscope/common/labthings/views.py
Normal file
0
openflexure_microscope/common/labthings/views.py
Normal file
|
|
@ -255,6 +255,7 @@ DEFAULT_CONFIG_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json"
|
|||
|
||||
USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure")
|
||||
USER_CONFIG_FILE_PATH = os.path.join(USER_CONFIG_DIR, "microscope_settings.json")
|
||||
USER_PLUGINS_PATH = os.path.join(USER_CONFIG_DIR, "microscope_plugins", "__init__.py")
|
||||
|
||||
# Load the default config
|
||||
with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue