Implemented root API v2 routes
This commit is contained in:
parent
5aa783c269
commit
6581612312
27 changed files with 1205 additions and 148 deletions
|
|
@ -21,6 +21,7 @@ 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.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
|
||||
|
|
@ -141,6 +142,31 @@ app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1"))
|
|||
task_blueprint = blueprints.task.construct_blueprint(api_microscope)
|
||||
app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1"))
|
||||
|
||||
### V2
|
||||
# Tasks routes
|
||||
v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope)
|
||||
app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "v2"))
|
||||
|
||||
# Captures routes
|
||||
v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope)
|
||||
app.register_blueprint(v2_captures_blueprint, url_prefix=uri("/captures", "v2"))
|
||||
|
||||
# Settings routes
|
||||
v2_settings_blueprint = v2.blueprints.settings.construct_blueprint(api_microscope)
|
||||
app.register_blueprint(v2_settings_blueprint, url_prefix=uri("/settings", "v2"))
|
||||
|
||||
# Status routes
|
||||
v2_status_blueprint = v2.blueprints.status.construct_blueprint(api_microscope)
|
||||
app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2"))
|
||||
|
||||
# Plugins routes
|
||||
v2_plugin_blueprint = v2.blueprints.plugins.construct_blueprint(api_microscope)
|
||||
app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2"))
|
||||
|
||||
# Tasks routes
|
||||
v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope)
|
||||
app.register_blueprint(v2_tasks_blueprint, url_prefix=uri("/tasks", "v2"))
|
||||
|
||||
|
||||
@app.route("/routes")
|
||||
def routes():
|
||||
|
|
@ -184,7 +210,7 @@ def cleanup():
|
|||
|
||||
# Save config
|
||||
logging.debug("Saving config for teardown...")
|
||||
api_microscope.save_config(backup=True)
|
||||
api_microscope.save_settings()
|
||||
|
||||
logging.debug("Settling...")
|
||||
time.sleep(0.5)
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class ConfigAPI(MicroscopeView):
|
|||
:>header Content-Type: application/json
|
||||
:status 200: state available
|
||||
"""
|
||||
return jsonify(self.microscope.read_config(json_safe=True))
|
||||
return jsonify(self.microscope.read_settings(json_safe=True))
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
|
|
@ -223,10 +223,10 @@ class ConfigAPI(MicroscopeView):
|
|||
logging.debug("Updating settings from POST request:")
|
||||
logging.debug(payload.json)
|
||||
|
||||
self.microscope.apply_config(payload.json)
|
||||
self.microscope.save_config()
|
||||
self.microscope.apply_settings(payload.json)
|
||||
self.microscope.save_settings()
|
||||
|
||||
return jsonify(self.microscope.read_config(json_safe=True))
|
||||
return jsonify(self.microscope.read_settings(json_safe=True))
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
|
|
|||
|
|
@ -130,16 +130,19 @@ class ListAPI(MicroscopeView):
|
|||
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
|
||||
)
|
||||
|
||||
metadata.update(
|
||||
{
|
||||
"microscope_settings": self.microscope.read_config(),
|
||||
# Inject system metadata
|
||||
system_metadata = {
|
||||
"microscope_settings": self.microscope.read_settings(),
|
||||
"microscope_state": self.microscope.state,
|
||||
"microscope_id": self.microscope.id,
|
||||
"microscope_name": self.microscope.name,
|
||||
}
|
||||
)
|
||||
output.system_metadata.update(system_metadata)
|
||||
|
||||
# Insert custom metadata
|
||||
output.put_metadata(metadata)
|
||||
|
||||
# Insert custom tags
|
||||
output.put_tags(tags)
|
||||
|
||||
return jsonify(output.state)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ZoomAPI(MicroscopeView):
|
|||
:<header Content-Type: application/json
|
||||
:status 200: preview started/stopped
|
||||
"""
|
||||
zoom_value = self.microscope.camera.state["zoom_value"]
|
||||
zoom_value = self.microscope.camera.status["zoom_value"]
|
||||
|
||||
return jsonify({"zoom_value": zoom_value})
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ class ZoomAPI(MicroscopeView):
|
|||
|
||||
self.microscope.camera.set_zoom(zoom_value)
|
||||
|
||||
return jsonify(self.microscope.camera.state)
|
||||
return jsonify(self.microscope.camera.status)
|
||||
|
||||
|
||||
class OverlayAPI(MicroscopeView):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from openflexure_microscope.api.views import MicroscopeViewPlugin
|
|||
from flask import Blueprint, jsonify
|
||||
from openflexure_microscope.api.views import MicroscopeView
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ class PluginFormAPI(MicroscopeView):
|
|||
A complete list of enabled plugins can be found in the microscope state.
|
||||
|
||||
"""
|
||||
out = self.microscope.plugin.forms
|
||||
out = self.microscope.plugins.forms
|
||||
return jsonify(out)
|
||||
|
||||
|
||||
|
|
@ -38,7 +39,10 @@ def construct_blueprint(microscope_obj):
|
|||
all_routes = []
|
||||
|
||||
# For each plugin attached to the microscope object
|
||||
for plugin_name, plugin_obj in microscope_obj.plugin.plugins:
|
||||
for plugin_representation in microscope_obj.plugins.active:
|
||||
|
||||
plugin_obj = plugin_representation["plugin"]
|
||||
plugin_name = plugin_representation["name"]
|
||||
|
||||
# If plugin contains valid endpoints
|
||||
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
|
||||
|
|
@ -94,7 +98,7 @@ def construct_blueprint(microscope_obj):
|
|||
if hasattr(plugin_obj, "api_form") and isinstance(
|
||||
plugin_obj.api_form, dict
|
||||
):
|
||||
api_form_info = plugin_obj.api_form
|
||||
api_form_info = copy.deepcopy(plugin_obj.api_form)
|
||||
api_form_info["id"] = plugin_name
|
||||
if "forms" in api_form_info and isinstance(
|
||||
api_form_info["forms"], list
|
||||
|
|
@ -110,8 +114,8 @@ def construct_blueprint(microscope_obj):
|
|||
)
|
||||
|
||||
# Store the complete form in Microscope().plugin.form
|
||||
microscope_obj.plugin.forms.append(api_form_info)
|
||||
print(microscope_obj.plugin.forms)
|
||||
microscope_obj.plugins.forms.append(api_form_info)
|
||||
print(microscope_obj.plugins.forms)
|
||||
|
||||
else:
|
||||
warnings.warn(
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import blueprints
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import captures, settings, status, tasks, stream, plugins, actions
|
||||
422
openflexure_microscope/api/v2/blueprints/captures.py
Normal file
422
openflexure_microscope/api/v2/blueprints/captures.py
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
||||
from openflexure_microscope.api.views import MicroscopeView
|
||||
from openflexure_microscope.utilities import filter_dict
|
||||
|
||||
from flask import jsonify, request, abort, url_for, redirect, send_file, Blueprint
|
||||
|
||||
|
||||
def captures_representation(capture_list: list, include_unavailable: bool = False):
|
||||
"""
|
||||
Generate a dictionary representation of all captures, including Flask route URLs
|
||||
|
||||
Args:
|
||||
capture_list (list): List of capture objects
|
||||
include_unavailable (bool): Include unavailable captures in response?
|
||||
|
||||
Returns:
|
||||
dict: Dictionary representation of all captures
|
||||
|
||||
"""
|
||||
if include_unavailable:
|
||||
captures = {image.id: image.state for image in capture_list}
|
||||
else:
|
||||
captures = {image.id: image.state for image in capture_list if image.state["available"]}
|
||||
|
||||
for capture_key, capture_repr in captures.items():
|
||||
# Add API routes to returned representations
|
||||
extra_state = {
|
||||
"links": {
|
||||
"properties": "{}".format(url_for(".capture", capture_id=capture_key)),
|
||||
"download": "{}".format(url_for(".capture_download", capture_id=capture_key, filename=capture_repr["filename"])),
|
||||
"tags": "{}".format(url_for(".capture_tags", capture_id=capture_key)),
|
||||
}
|
||||
}
|
||||
|
||||
captures[capture_key].update(extra_state)
|
||||
|
||||
return captures
|
||||
|
||||
|
||||
class ListAPI(MicroscopeView):
|
||||
def get(self):
|
||||
"""
|
||||
Get list of image captures.
|
||||
|
||||
.. :quickref: Captures; Get collection of captures
|
||||
|
||||
:>header Accept: application/json
|
||||
:query include_unavailable: return json representations of captures that have been completely deleted
|
||||
|
||||
:>jsonarr boolean available: availability of capture data
|
||||
:>jsonarr string filename: filename of capture
|
||||
:>jsonarr string id: unique id of the capture object
|
||||
:>jsonarr boolean keep_on_disk: keep the capture file on microscope after closing
|
||||
:>jsonarr boolean locked: file locked for modifications (mostly used for video recording)
|
||||
:>jsonarr string path: path on pi storage to the capture file, if available
|
||||
:>jsonarr boolean stream: capture stored in-memory as a BytesIO stream
|
||||
:>jsonarr json uri: - **download** *(string)*: api uri to the capture file download
|
||||
- **state** *(string)*: api uri to the capture json representation
|
||||
|
||||
:>header Content-Type: application/json
|
||||
:status 200: capture found
|
||||
:status 404: no capture found with that id
|
||||
"""
|
||||
include_unavailable = get_bool(request.args.get("include_unavailable"))
|
||||
|
||||
representation = captures_representation(self.microscope.camera.images, include_unavailable=include_unavailable)
|
||||
|
||||
return jsonify(representation)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete all captures
|
||||
|
||||
.. :quickref: Captures; Delete all captures
|
||||
"""
|
||||
for image in self.microscope.camera.images:
|
||||
image.delete()
|
||||
|
||||
captures = [image.state for image in self.microscope.camera.images]
|
||||
|
||||
return jsonify(captures)
|
||||
|
||||
|
||||
class CaptureAPI(MicroscopeView):
|
||||
def get(self, capture_id):
|
||||
"""
|
||||
Get JSON representation of a capture
|
||||
|
||||
.. :quickref: Capture; Get capture
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/ HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"available": true,
|
||||
"bytestream": false,
|
||||
"keep_on_disk": false,
|
||||
"locked": false,
|
||||
"metadata": {
|
||||
"filename": "2018-11-16_10-21-53.jpeg",
|
||||
"id": "d0b2067abbb946f19351e075c5e7cd5b",
|
||||
"path": "capture/2018-11-16_10-21-53.jpeg",
|
||||
"time": "2018-11-16_10-21-53"
|
||||
}
|
||||
"uri": {
|
||||
"download": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/download",
|
||||
"state": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/"
|
||||
}
|
||||
}
|
||||
|
||||
:>json boolean available: availability of capture data
|
||||
:>json string filename: filename of capture
|
||||
:>json string id: unique id of the capture object
|
||||
:>json boolean keep_on_disk: keep the capture file on microscope after closing
|
||||
:>json boolean locked: file locked for modifications (mostly used for video recording)
|
||||
:>json string path: path on pi storage to the capture file, if available
|
||||
:>json boolean stream: capture stored in-memory as a BytesIO stream
|
||||
:>json json uri: - **download** *(string)*: api uri to the capture file download
|
||||
- **state** *(string)*: api uri to the capture json representation
|
||||
|
||||
"""
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
# Get capture state
|
||||
capture_metadata = capture_obj.state
|
||||
|
||||
# Add API routes to returned state
|
||||
uri_dict = {
|
||||
"uri": {
|
||||
"state": "{}".format(url_for(".capture", capture_id=capture_obj.id))
|
||||
}
|
||||
}
|
||||
|
||||
# If available, also add download link
|
||||
if capture_metadata["available"]:
|
||||
uri_dict["uri"]["download"] = "{}download/{}".format(
|
||||
url_for(".capture", capture_id=capture_obj.id), capture_obj.filename
|
||||
)
|
||||
|
||||
capture_metadata.update(uri_dict)
|
||||
|
||||
return jsonify(capture_metadata)
|
||||
|
||||
def delete(self, capture_id):
|
||||
"""
|
||||
Delete all capture data from the Pi, even if `keep_on_disk=true;`.
|
||||
|
||||
.. :quickref: Capture; Delete capture.
|
||||
"""
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
capture_obj.delete()
|
||||
|
||||
return jsonify({"return": capture_id})
|
||||
|
||||
def put(self, capture_id):
|
||||
"""
|
||||
Add arbitrary metadata to the capture
|
||||
|
||||
.. :quickref: Capture; Update capture metadata
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
{
|
||||
"user_id": "ofm_1234",
|
||||
"patient_number": 1452,
|
||||
}
|
||||
|
||||
:>header Accept: application/json
|
||||
|
||||
:<header Content-Type: application/json
|
||||
:status 200: metadata updated
|
||||
"""
|
||||
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
data_dict = JsonResponse(request).json
|
||||
|
||||
capture_obj.put_metadata(data_dict)
|
||||
|
||||
capture_metadata = capture_obj.state
|
||||
|
||||
return jsonify(capture_metadata)
|
||||
|
||||
|
||||
class DownloadRedirectAPI(MicroscopeView):
|
||||
def get(self, capture_id):
|
||||
"""
|
||||
Return image data for a capture.
|
||||
|
||||
Return capture data as an image file with the requested filename.
|
||||
I.e., `/(capture_id)/download/foo.jpeg` will download the image as
|
||||
`foo.jpeg`, regardless of the capture's initially set filename.
|
||||
|
||||
Route automatically redirects to download the capture under it's currently set filename.
|
||||
I.e., `/(capture_id)/download` will
|
||||
redirect to `/(capture_id)/download/(filename)`.
|
||||
|
||||
.. :quickref: Capture; Download capture image
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/download/2018-11-20_16-04-17.jpeg HTTP/1.1
|
||||
Accept: image/jpeg
|
||||
|
||||
:>header Accept: image/jpeg
|
||||
:query thumbnail: return an image thumbnail e.g. ?thumbnail=true
|
||||
|
||||
:>header Content-Type: image/jpeg
|
||||
:status 200: capture data found
|
||||
:status 404: no capture found with that id
|
||||
|
||||
"""
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj or not capture_obj.state["available"]:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
thumbnail = get_bool(request.args.get("thumbnail"))
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".capture_download",
|
||||
capture_id=capture_id,
|
||||
filename=capture_obj.filename,
|
||||
thumbnail=thumbnail,
|
||||
),
|
||||
code=307,
|
||||
)
|
||||
|
||||
|
||||
class DownloadAPI(MicroscopeView):
|
||||
def get(self, capture_id, filename):
|
||||
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj or not capture_obj.state["available"]:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
thumbnail = get_bool(request.args.get("thumbnail"))
|
||||
|
||||
# If no filename is specified, redirect to the capture's currently set filename
|
||||
if not filename:
|
||||
return redirect(
|
||||
url_for(
|
||||
"capture_download",
|
||||
capture_id=capture_id,
|
||||
filename=capture_obj.filename,
|
||||
thumbnail=thumbnail,
|
||||
),
|
||||
code=307,
|
||||
)
|
||||
|
||||
# Download the image data using the requested filename
|
||||
if thumbnail:
|
||||
img = capture_obj.thumbnail
|
||||
else:
|
||||
img = capture_obj.data
|
||||
|
||||
return send_file(img, mimetype="image/jpeg")
|
||||
|
||||
|
||||
class TagsAPI(MicroscopeView):
|
||||
def get(self, capture_id):
|
||||
"""
|
||||
Return tag list for a capture.
|
||||
|
||||
.. :quickref: Capture; Show capture tags
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
||||
Accept: text/json
|
||||
|
||||
:>header Accept: text/json
|
||||
|
||||
:>header Content-Type: text/json
|
||||
:status 200: capture data found
|
||||
:status 404: no capture found with that id
|
||||
"""
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj or not capture_obj.state["available"]:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
return jsonify(capture_obj.tags)
|
||||
|
||||
def put(self, capture_id):
|
||||
"""
|
||||
Add tags to the capture
|
||||
|
||||
.. :quickref: Capture; Update capture tags
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
["tests", "mytag", "someothertag"]
|
||||
|
||||
:>header Accept: application/json
|
||||
|
||||
:<header Content-Type: application/json
|
||||
:status 200: metadata updated
|
||||
"""
|
||||
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj or not capture_obj.state["available"]:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
data_dict = JsonResponse(request).json
|
||||
|
||||
if type(data_dict) != list:
|
||||
return abort(400)
|
||||
|
||||
capture_obj.put_tags(data_dict)
|
||||
|
||||
return jsonify(capture_obj.tags)
|
||||
|
||||
def delete(self, capture_id):
|
||||
"""
|
||||
Delete tags from the capture
|
||||
|
||||
.. :quickref: Capture; Delete tags.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
["tests", "mytag"]
|
||||
|
||||
:>header Accept: application/json
|
||||
|
||||
:<header Content-Type: application/json
|
||||
:status 200: metadata updated
|
||||
"""
|
||||
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
data_dict = JsonResponse(request).json
|
||||
|
||||
if type(data_dict) != list:
|
||||
return abort(400)
|
||||
|
||||
for tag in data_dict:
|
||||
capture_obj.delete_tag(str(tag))
|
||||
|
||||
return jsonify(capture_obj.tags)
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
blueprint = Blueprint("captures_blueprint", __name__)
|
||||
|
||||
# Tag routes
|
||||
blueprint.add_url_rule(
|
||||
"/<capture_id>/tags",
|
||||
view_func=TagsAPI.as_view("capture_tags", microscope=microscope_obj),
|
||||
)
|
||||
|
||||
# Capture routes
|
||||
blueprint.add_url_rule(
|
||||
"/<capture_id>/download/<filename>",
|
||||
view_func=DownloadAPI.as_view(
|
||||
"capture_download", microscope=microscope_obj
|
||||
),
|
||||
)
|
||||
|
||||
blueprint.add_url_rule(
|
||||
"/<capture_id>/download",
|
||||
view_func=DownloadRedirectAPI.as_view(
|
||||
"capture_download_redirect", microscope=microscope_obj
|
||||
),
|
||||
)
|
||||
|
||||
blueprint.add_url_rule(
|
||||
"/<capture_id>/",
|
||||
view_func=CaptureAPI.as_view("capture", microscope=microscope_obj),
|
||||
)
|
||||
|
||||
blueprint.add_url_rule(
|
||||
"/",
|
||||
view_func=ListAPI.as_view("capture_list", microscope=microscope_obj),
|
||||
)
|
||||
return blueprint
|
||||
151
openflexure_microscope/api/v2/blueprints/plugins.py
Normal file
151
openflexure_microscope/api/v2/blueprints/plugins.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin
|
||||
from openflexure_microscope.api.views import MicroscopeViewPlugin
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
from openflexure_microscope.api.views import MicroscopeView
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
|
||||
def plugins_representation(plugin_loader_object: PluginLoader):
|
||||
"""
|
||||
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_loader_object.active:
|
||||
d = {
|
||||
"name": plugin["name"],
|
||||
"plugin": str(plugin["plugin"]),
|
||||
"routes": plugin["routes"],
|
||||
"form": plugin["form"]
|
||||
}
|
||||
plugins.append(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.
|
||||
|
||||
"""
|
||||
return jsonify(plugins_representation(self.microscope.plugins))
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
||||
blueprint = Blueprint("plugins_blueprint", __name__)
|
||||
|
||||
# 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),
|
||||
)
|
||||
|
||||
all_routes = []
|
||||
|
||||
# For each plugin attached to the microscope object
|
||||
for plugin_representation in microscope_obj.plugins.active:
|
||||
|
||||
plugin_obj = plugin_representation["plugin"]
|
||||
plugin_name = plugin_representation["name"]
|
||||
|
||||
# If plugin contains valid endpoints
|
||||
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
|
||||
|
||||
# We'll keep a record of how each route was expanded
|
||||
expanded_routes = {}
|
||||
|
||||
# For each defined endpoint
|
||||
for view_route, view_class in plugin_obj.api_views.items():
|
||||
|
||||
# Remove all leading slashes from view route
|
||||
cleaned_route = view_route
|
||||
while cleaned_route[0] == "/":
|
||||
cleaned_route = cleaned_route[1:]
|
||||
|
||||
# Construct a full view route from the plugin name
|
||||
full_view_route = "/{}/{}".format(plugin_name, cleaned_route)
|
||||
logging.debug(full_view_route)
|
||||
|
||||
# Record how the view_route got expanded
|
||||
expanded_routes[view_route] = full_view_route
|
||||
|
||||
# Check if endpoint name clashes
|
||||
if full_view_route not in all_routes and issubclass(
|
||||
view_class, MicroscopeViewPlugin
|
||||
):
|
||||
# Add route to main route dictionary
|
||||
all_routes.append(full_view_route)
|
||||
|
||||
# Create a Python-safe name for the route
|
||||
plugin_route_id = "plugin{}".format(full_view_route).replace(
|
||||
"/", "_"
|
||||
)
|
||||
|
||||
# Add route to the plugins blueprint
|
||||
blueprint.add_url_rule(
|
||||
full_view_route,
|
||||
view_func=view_class.as_view(
|
||||
plugin_route_id,
|
||||
microscope=microscope_obj,
|
||||
plugin=plugin_obj,
|
||||
),
|
||||
)
|
||||
|
||||
# Add route to the plugin representation dictionary
|
||||
plugin_representation["routes"].append(full_view_route)
|
||||
|
||||
else:
|
||||
warnings.warn(
|
||||
"An endpoint /{} has already been loaded. Skipping {}.".format(
|
||||
full_view_route, view_class
|
||||
)
|
||||
)
|
||||
|
||||
# If plugin includes an API form
|
||||
if hasattr(plugin_obj, "api_form") and isinstance(
|
||||
plugin_obj.api_form, dict
|
||||
):
|
||||
# TODO: We deep copy this to avoid clashing between API versions. Can be removed when v1 is removed.
|
||||
api_form_info = copy.deepcopy(plugin_obj.api_form)
|
||||
api_form_info["id"] = plugin_name
|
||||
if "forms" in api_form_info and isinstance(
|
||||
api_form_info["forms"], list
|
||||
):
|
||||
for form in api_form_info["forms"]:
|
||||
if "route" in form and form["route"] in expanded_routes.keys():
|
||||
form["route"] = expanded_routes[form["route"]]
|
||||
else:
|
||||
logging.warn(
|
||||
"No valid expandable route found for {}".format(
|
||||
form["route"]
|
||||
)
|
||||
)
|
||||
|
||||
# Store the complete form in Microscope().plugin.form
|
||||
plugin_representation["form"] = api_form_info
|
||||
print(microscope_obj.plugins.forms)
|
||||
|
||||
else:
|
||||
warnings.warn(
|
||||
"No valid 'api_views' dictionary found in {}".format(plugin_obj)
|
||||
)
|
||||
return blueprint
|
||||
153
openflexure_microscope/api/v2/blueprints/settings.py
Normal file
153
openflexure_microscope/api/v2/blueprints/settings.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
from openflexure_microscope.api.utilities import gen, JsonResponse
|
||||
from openflexure_microscope.api.views import MicroscopeView
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
import logging
|
||||
|
||||
|
||||
class SettingsAPI(MicroscopeView):
|
||||
def get(self):
|
||||
"""
|
||||
JSON representation of the microscope settings.
|
||||
|
||||
.. :quickref: Settings; Get microscope settings
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /settings/ HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "0e2c6fac5421429aac67c7903107bdd8",
|
||||
"name": "docuscope-2000",
|
||||
"fov": [4100, 3146],
|
||||
"camera_settings": {
|
||||
"image_resolution": [2592, 1944],
|
||||
"numpy_resolution": [1312, 976],
|
||||
"video_resolution": [832, 624],
|
||||
"jpeg_quality": 75,
|
||||
"picamera_settings": {
|
||||
"analog_gain": 1.0,
|
||||
"digital_gain": 1.0,
|
||||
"awb_gains": [0.92578125, 2.94921875],
|
||||
"awb_mode": "off",
|
||||
"exposure_mode": "off",
|
||||
"framerate": 24.0,
|
||||
"saturation": 0,
|
||||
"shutter_speed": 5378
|
||||
},
|
||||
},
|
||||
"stage_settings": {
|
||||
"backlash": {
|
||||
"x": 256,
|
||||
"y": 256,
|
||||
"z": 0
|
||||
}
|
||||
}
|
||||
"plugins": [
|
||||
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
||||
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
||||
"openflexure_microscope.plugins.default.camera_calibration:Plugin"
|
||||
]
|
||||
}
|
||||
|
||||
:>header Accept: application/json
|
||||
:>header Content-Type: application/json
|
||||
:status 200: state available
|
||||
"""
|
||||
return jsonify(self.microscope.read_settings())
|
||||
|
||||
def put(self):
|
||||
"""
|
||||
Modify microscope configuration
|
||||
|
||||
.. :quickref: Config; Set microscope config
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /config HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
{
|
||||
"id": "0e2c6fac5421429aac67c7903107bdd8",
|
||||
"name": "docuscope-2000",
|
||||
"fov": [4100, 3146],
|
||||
"camera_settings": {
|
||||
"image_resolution": [2592, 1944],
|
||||
"numpy_resolution": [1312, 976],
|
||||
"video_resolution": [832, 624],
|
||||
"jpeg_quality": 75,
|
||||
"picamera_settings": {
|
||||
"analog_gain": 1.0,
|
||||
"digital_gain": 1.0,
|
||||
"awb_gains": [0.92578125, 2.94921875],
|
||||
"awb_mode": "off",
|
||||
"exposure_mode": "off",
|
||||
"framerate": 24.0,
|
||||
"saturation": 0,
|
||||
"shutter_speed": 5378
|
||||
},
|
||||
},
|
||||
"stage_settings": {
|
||||
"backlash": {
|
||||
"x": 256,
|
||||
"y": 256,
|
||||
"z": 0
|
||||
}
|
||||
}
|
||||
"plugins": [
|
||||
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
||||
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
||||
"openflexure_microscope.plugins.default.camera_calibration:Plugin"
|
||||
]
|
||||
}
|
||||
|
||||
:>header Accept: application/json
|
||||
|
||||
:<json string id: Unique string identifier of the microscope.
|
||||
:<json string name: Friendly name for the microscope
|
||||
:<json array fov: Field of view (motor steps per full width and height of frame)
|
||||
:<json json camera_settings: - **image_resolution** *(array)*: Resolution of full image captures
|
||||
- **numpy_resolution** *(array)*: Resolution of full numpy array captures
|
||||
- **video_resolution** *(array)*: Resolution of video recordings, low res image captures, and the preview stream
|
||||
- **jpeg_quality** *(int)*: Quality in which to store JPEG capture data
|
||||
- **picamera_settings** *(json)*: Key-value pairs to apply directly to any attached PiCamera object
|
||||
:<json json stage_settings: - **backlash** *(json)*: x, y, and z backlash compensation, in motor steps
|
||||
:<json array plugins: Array of plugin paths to load. Requires reloading the microscope object after applying
|
||||
|
||||
:<header Content-Type: application/json
|
||||
:status 200: capture created
|
||||
|
||||
"""
|
||||
payload = JsonResponse(request)
|
||||
|
||||
logging.debug("Updating settings from PUT request:")
|
||||
logging.debug(payload.json)
|
||||
|
||||
self.microscope.apply_settings(payload.json)
|
||||
self.microscope.save_settings()
|
||||
|
||||
return jsonify(self.microscope.read_settings(json_safe=True))
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
||||
blueprint = Blueprint("v2_settings_blueprint", __name__)
|
||||
|
||||
blueprint.add_url_rule(
|
||||
"/", view_func=SettingsAPI.as_view("settings", microscope=microscope_obj)
|
||||
)
|
||||
|
||||
return blueprint
|
||||
64
openflexure_microscope/api/v2/blueprints/status.py
Normal file
64
openflexure_microscope/api/v2/blueprints/status.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from openflexure_microscope.api.views import MicroscopeView
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
|
||||
class StatusAPI(MicroscopeView):
|
||||
def get(self):
|
||||
"""
|
||||
JSON representation of the microscope status (read-only properties, modifiable with actions)
|
||||
|
||||
.. :quickref: Status; Microscope status
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /status/ HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera": {
|
||||
"preview_active": false,
|
||||
"record_active": false,
|
||||
"stream_active": true
|
||||
},
|
||||
"plugin": {},
|
||||
"stage": {
|
||||
"backlash": {
|
||||
"x": 128,
|
||||
"y": 128,
|
||||
"z": 128
|
||||
},
|
||||
"position": {
|
||||
"x": -8080,
|
||||
"y": 5665,
|
||||
"z": -12600
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:>header Accept: application/json
|
||||
:>header Content-Type: application/json
|
||||
:status 200: state available
|
||||
"""
|
||||
return jsonify(self.microscope.status)
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
||||
blueprint = Blueprint("v2_status_blueprint", __name__)
|
||||
|
||||
blueprint.add_url_rule(
|
||||
"/", view_func=StatusAPI.as_view("status", microscope=microscope_obj)
|
||||
)
|
||||
|
||||
return blueprint
|
||||
57
openflexure_microscope/api/v2/blueprints/stream.py
Normal file
57
openflexure_microscope/api/v2/blueprints/stream.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from openflexure_microscope.api.utilities import gen, JsonResponse
|
||||
from openflexure_microscope.api.views import MicroscopeView
|
||||
|
||||
from flask import Response, Blueprint, jsonify, request
|
||||
|
||||
|
||||
class StreamAPI(MicroscopeView):
|
||||
def get(self):
|
||||
"""
|
||||
Real-time MJPEG stream from the microscope camera
|
||||
|
||||
.. :quickref: Stream; Camera MJPEG stream
|
||||
|
||||
:>header Accept: image/jpeg
|
||||
:>header Content-Type: image/jpeg
|
||||
:status 200: stream active
|
||||
"""
|
||||
# Restart stream worker thread
|
||||
self.microscope.camera.start_worker()
|
||||
|
||||
return Response(
|
||||
gen(self.microscope.camera),
|
||||
mimetype="multipart/x-mixed-replace; boundary=frame",
|
||||
)
|
||||
|
||||
|
||||
class SnapshotAPI(MicroscopeView):
|
||||
def get(self):
|
||||
"""
|
||||
Single snapshot from the camera stream
|
||||
|
||||
.. :quickref: Stream; Camera snapshot
|
||||
|
||||
:>header Accept: image/jpeg
|
||||
:>header Content-Type: image/jpeg
|
||||
:status 200: stream active
|
||||
"""
|
||||
# Restart stream worker thread
|
||||
self.microscope.camera.start_worker()
|
||||
|
||||
return Response(self.microscope.camera.get_frame(), mimetype="image/jpeg")
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
||||
blueprint = Blueprint("v2_stream_blueprint", __name__)
|
||||
|
||||
blueprint.add_url_rule(
|
||||
"/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj)
|
||||
)
|
||||
|
||||
blueprint.add_url_rule(
|
||||
"/snapshot",
|
||||
view_func=SnapshotAPI.as_view("snapshot", microscope=microscope_obj),
|
||||
)
|
||||
|
||||
return blueprint
|
||||
160
openflexure_microscope/api/v2/blueprints/tasks.py
Normal file
160
openflexure_microscope/api/v2/blueprints/tasks.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
from openflexure_microscope.api.views import MethodView
|
||||
from flask import jsonify, abort, Blueprint, url_for
|
||||
|
||||
from openflexure_microscope.common import tasks
|
||||
|
||||
|
||||
def tasks_representation():
|
||||
"""
|
||||
Generate a dictionary representation of all tasks, including Flask route URLs
|
||||
|
||||
Returns:
|
||||
dict: Dictionary representation of all tasks
|
||||
"""
|
||||
tasks_dict = tasks.states()
|
||||
|
||||
for task_key, task_repr in tasks_dict.items():
|
||||
# Add API routes to returned representations
|
||||
extra_state = {
|
||||
"links": {
|
||||
"properties": "{}".format(url_for(".task", task_id=task_key)),
|
||||
}
|
||||
}
|
||||
|
||||
tasks_dict[task_key].update(extra_state)
|
||||
|
||||
return tasks_dict
|
||||
|
||||
|
||||
class TaskListAPI(MethodView):
|
||||
def get(self):
|
||||
"""
|
||||
Get list of long-running tasks.
|
||||
|
||||
.. :quickref: Tasks; Get collection of tasks
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"end_time": "2019-01-23 16-33-33",
|
||||
"id": "db13a66787e1419bb06b1504e4d80b0c",
|
||||
"return": [
|
||||
0.848622546386467,
|
||||
0.6106785018091292,
|
||||
],
|
||||
"start_time": "2019-01-23 16-33-13",
|
||||
"status": "success"
|
||||
},
|
||||
{
|
||||
"end_time": null,
|
||||
"id": "df46558cc8844924821bd0181881871e",
|
||||
"return": null,
|
||||
"start_time": "2019-01-23 16-34-54",
|
||||
"status": "running"
|
||||
}
|
||||
]
|
||||
|
||||
:>header Accept: application/json
|
||||
:query include_unavailable: return json representations of captures that have been completely deleted
|
||||
|
||||
:>header Content-Type: application/json
|
||||
"""
|
||||
|
||||
return jsonify(tasks_representation())
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Clean list of long-running tasks (running tasks persist).
|
||||
|
||||
.. :quickref: Tasks; Clean collection of tasks
|
||||
|
||||
:>header Accept: application/json
|
||||
|
||||
:>header Content-Type: application/json
|
||||
"""
|
||||
|
||||
tasks.cleanup_tasks()
|
||||
|
||||
return jsonify(tasks_representation())
|
||||
|
||||
|
||||
class TaskAPI(MethodView):
|
||||
def get(self, task_id):
|
||||
"""
|
||||
Get JSON representation of a task
|
||||
|
||||
.. :quickref: Tasks; Get task
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /task/db13a66787e1419bb06b1504e4d80b0c/ HTTP/1.1
|
||||
Accept: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"end_time": "2019-01-23 16-33-33",
|
||||
"id": "db13a66787e1419bb06b1504e4d80b0c",
|
||||
"return": [
|
||||
0.848622546386467,
|
||||
0.6106785018091292,
|
||||
],
|
||||
"start_time": "2019-01-23 16-33-13",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
task = tasks.tasks()[task_id]
|
||||
except KeyError:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
# Return task state
|
||||
return jsonify(task.state)
|
||||
|
||||
def delete(self, task_id):
|
||||
"""
|
||||
Terminate a particular task.
|
||||
|
||||
.. :quickref: Tasks; Terminate a task
|
||||
|
||||
:>header Accept: application/json
|
||||
|
||||
:>header Content-Type: application/json
|
||||
"""
|
||||
|
||||
try:
|
||||
task = tasks.tasks()[task_id]
|
||||
except KeyError:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
task.terminate()
|
||||
|
||||
return jsonify(task.state)
|
||||
|
||||
|
||||
def construct_blueprint(microscope_obj):
|
||||
|
||||
blueprint = Blueprint("v2_tasks_blueprint", __name__)
|
||||
|
||||
blueprint.add_url_rule("/", view_func=TaskListAPI.as_view("task_list"))
|
||||
|
||||
blueprint.add_url_rule("/<task_id>/", view_func=TaskAPI.as_view("task"))
|
||||
|
||||
return blueprint
|
||||
Loading…
Add table
Add a link
Reference in a new issue