diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index e69de29b..00000000
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..5d5425e6
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,11 @@
+repos:
+ - repo: https://github.com/psf/black
+ rev: 19.10b0
+ hooks:
+ - id: black
+ - repo: https://github.com/timothycrosley/isort
+ rev: 5.4.2
+ hooks:
+ - id: isort
+ additional_dependencies: [toml]
+ exclude: ^.*/?setup\.py$
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index c9cbf4ad..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,20 +0,0 @@
-FROM python:3.6
-
-ENV PYTHONUNBUFFERED=1 \
- POETRY_VERSION=1.0.3 \
- POETRY_HOME="/opt/poetry" \
- POETRY_VIRTUALENVS_IN_PROJECT=true \
- POETRY_NO_INTERACTION=1
-
-ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
-
-WORKDIR /app
-
-RUN curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
-
-COPY . ./
-
-RUN poetry install --no-dev --no-interaction
-
-EXPOSE 5000
-CMD ["poetry", "run", "python", "-m", "openflexure_microscope.api.app"]
diff --git a/README.md b/README.md
index e3327bce..55b83c66 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,17 @@ This includes installing the server in a mode better suited for active developme
* `poetry install`
* `poetry run build_static`
+### Pre-commit hooks
+
+We make use of pre-commit hooks to run code analysis before committing to the codebase.
+
+The simplest way to ensure this works is to install pre-commits into your current Python environment:
+
+* `pip3 install pre-commit`
+* `pre-commit install`
+
+To run pre-commit analysis manually, you can run `pre-commit run`.
+
### Node installation
* Note, building the static interface will require a valid Node.js installation
diff --git a/docs/source/conf.py b/docs/source/conf.py
index fe3f927f..4fd4b0c0 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -38,7 +38,7 @@ sys.modules.update((mod_name, Mock()) for mod_name in mock_imports)
# -- Project information -----------------------------------------------------
project = "OpenFlexure Microscope Software"
-copyright = "2018, Bath Open Instrumentation Group"
+copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin
author = "Bath Open Instrumentation Group"
# The short X.Y version
diff --git a/docs/source/extensions/example_extension/04_properties.py b/docs/source/extensions/example_extension/04_properties.py
index ec13cc3c..3bedbad0 100644
--- a/docs/source/extensions/example_extension/04_properties.py
+++ b/docs/source/extensions/example_extension/04_properties.py
@@ -1,6 +1,6 @@
from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
-from labthings.views import PropertyView, View
+from labthings.views import PropertyView
## Extension methods
diff --git a/docs/source/extensions/example_extension/05_actions.py b/docs/source/extensions/example_extension/05_actions.py
index 3980e531..52b1b024 100644
--- a/docs/source/extensions/example_extension/05_actions.py
+++ b/docs/source/extensions/example_extension/05_actions.py
@@ -3,7 +3,7 @@ import io # Used in our capture action
from flask import send_file # Used to send images from our server
from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
-from labthings.views import ActionView, PropertyView, View
+from labthings.views import ActionView, PropertyView
## Extension methods
diff --git a/docs/source/extensions/example_extension/06_tasks_locks.py b/docs/source/extensions/example_extension/06_tasks_locks.py
index e17ac971..710b4e73 100644
--- a/docs/source/extensions/example_extension/06_tasks_locks.py
+++ b/docs/source/extensions/example_extension/06_tasks_locks.py
@@ -1,16 +1,8 @@
-import io # Used in our capture action
import time # Used in our timelapse function
-from flask import send_file # Used to send images from our server
-from labthings import (
- Schema,
- current_action,
- fields,
- find_component,
- update_action_progress,
-)
+from labthings import current_action, fields, find_component, update_action_progress
from labthings.extensions import BaseExtension
-from labthings.views import ActionView, PropertyView, View
+from labthings.views import ActionView
# Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename
diff --git a/docs/source/extensions/example_extension/07_ev_gui.py b/docs/source/extensions/example_extension/07_ev_gui.py
index ea2c9c9b..a71d9053 100644
--- a/docs/source/extensions/example_extension/07_ev_gui.py
+++ b/docs/source/extensions/example_extension/07_ev_gui.py
@@ -1,16 +1,8 @@
-import io # Used in our capture action
import time # Used in our timelapse function
-from flask import send_file # Used to send images from our server
-from labthings import (
- Schema,
- current_action,
- fields,
- find_component,
- update_action_progress,
-)
+from labthings import current_action, fields, find_component, update_action_progress
from labthings.extensions import BaseExtension
-from labthings.views import ActionView, PropertyView, View
+from labthings.views import ActionView
# Used to convert our GUI dictionary into a complete eV extension GUI
from openflexure_microscope.api.utilities.gui import build_gui
diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py
index 26d7f5c1..ebb77d19 100644
--- a/openflexure_microscope/api/app.py
+++ b/openflexure_microscope/api/app.py
@@ -24,13 +24,13 @@ import pkg_resources
from flask import abort, send_file
from flask_cors import CORS, cross_origin
from labthings import create_app
-from labthings.views import View
from labthings.extensions import find_extensions
+from labthings.views import View
from openflexure_microscope.api.microscope import default_microscope as api_microscope
from openflexure_microscope.api.utilities import init_default_extensions, list_routes
from openflexure_microscope.api.v2 import views
-from openflexure_microscope.config import JSONEncoder
+from openflexure_microscope.json import JSONEncoder
from openflexure_microscope.paths import (
OPENFLEXURE_EXTENSIONS_PATH,
OPENFLEXURE_VAR_PATH,
diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py
index 507a9fac..a9e3f0e4 100644
--- a/openflexure_microscope/api/default_extensions/autofocus.py
+++ b/openflexure_microscope/api/default_extensions/autofocus.py
@@ -83,7 +83,7 @@ class JPEGSharpnessMonitor:
raise e
if stop < 1:
stop = len(jpeg_times)
- logging.debug("changing stop to {}".format(stop))
+ logging.debug("changing stop to %s", (stop))
jpeg_times = jpeg_times[start:stop]
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
@@ -258,13 +258,12 @@ def fast_up_down_up_autofocus(
# Ensure the MJPEG stream has started
microscope.camera.start_stream_recording()
- df = dz # TODO: refactor so I actually use dz in the code below!
logging.debug("Initial move")
if initial_move_up:
- m.focus_rel(df / 2)
+ m.focus_rel(dz / 2)
# move down
logging.debug("Move down")
- i, z = m.focus_rel(-df)
+ i, z = m.focus_rel(-dz)
# now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack
_, jz, js = m.move_data(i)
@@ -285,16 +284,14 @@ def fast_up_down_up_autofocus(
inow = np.argmax(
js < current_js
) # use the curve we recorded to estimate our position
- # TODO: fancy interpolation stuff
# So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow]
logging.debug("Correction move")
correction_move = best_z + target_z - jz[inow]
logging.debug(
- "Fast autofocus scan: correcting backlash by moving {} steps".format(
- correction_move
- )
+ "Fast autofocus scan: correcting backlash by moving %s steps",
+ (correction_move),
)
m.focus_rel(correction_move)
return m.data_dict()
diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py
index 28b04c02..fd18cf46 100644
--- a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py
+++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py
@@ -63,17 +63,15 @@ def auto_expose_and_freeze_settings(camera):
logging.info("Freezing the camera settings...")
camera.shutter_speed = camera.exposure_speed
- logging.info("Shutter speed = {}".format(camera.shutter_speed))
+ logging.info("Shutter speed = %s", (camera.shutter_speed))
camera.exposure_mode = "off"
logging.info("Auto exposure disabled")
g = camera.awb_gains
camera.awb_mode = "off"
camera.awb_gains = g
- logging.info("Auto white balance disabled, gains are {}".format(g))
+ logging.info("Auto white balance disabled, gains are %s", (g))
logging.info(
- "Analogue gain: {}, Digital gain: {}".format(
- camera.analog_gain, camera.digital_gain
- )
+ "Analogue gain: %s, Digital gain: %s", camera.analog_gain, camera.digital_gain
)
adjust_exposure_to_setpoint(camera, 215)
@@ -100,7 +98,7 @@ def lst_from_channels(channels):
# lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
lst_resolution = [(r // 64) + 1 for r in full_resolution]
# NB the size of the LST is 1/64th of the image, but rounded UP.
- logging.info("Generating a lens shading table at {}x{}".format(*lst_resolution))
+ logging.info("Generating a lens shading table at %sx%s", *lst_resolution)
lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float)
for i in range(lens_shading.shape[0]):
image_channel = channels[i, :, :]
@@ -118,9 +116,12 @@ def lst_from_channels(channels):
image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
) # Pad image to the right and bottom
logging.info(
- "Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(
- iw, ih, lw * 32, lh * 32, padded_image_channel.shape
- )
+ "Channel shape: %sx%s, shading table shape: %sx%s, after padding %s",
+ iw,
+ ih,
+ lw * 32,
+ lh * 32,
+ padded_image_channel.shape,
)
# Next, fill the shading table (except edge pixels). Please excuse the
# for loop - I know it's not fast but this code needn't be!
diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py
index 9a292f11..544d8017 100644
--- a/openflexure_microscope/api/default_extensions/scan.py
+++ b/openflexure_microscope/api/default_extensions/scan.py
@@ -16,6 +16,7 @@ from labthings import (
from labthings.extensions import BaseExtension
from labthings.views import ActionView
+from openflexure_microscope.api.v2.views.actions.camera import FullCaptureArgs
from openflexure_microscope.captures.capture_manager import generate_basename
from openflexure_microscope.devel import abort
@@ -216,7 +217,7 @@ class ScanExtension(BaseExtension):
for x_y in line:
# Move to new grid position without changing z
- logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z]))
+ logging.debug("Moving to step %s", ([x_y[0], x_y[1], next_z]))
microscope.stage.move_abs([x_y[0], x_y[1], next_z])
# Refocus
if autofocus_enabled:
@@ -270,7 +271,7 @@ class ScanExtension(BaseExtension):
# Make sure we use our current best estimate of focus (i.e. the current position) next point
next_z = microscope.stage.position[2]
- logging.debug("Returning to {}".format(initial_position))
+ logging.debug("Returning to %s", (initial_position))
microscope.stage.move_abs(initial_position)
end = time.time()
@@ -328,34 +329,29 @@ class ScanExtension(BaseExtension):
return
if i != steps - 1:
- logging.debug("Moving z by {}".format(step_size))
+ logging.debug("Moving z by %s", (step_size))
microscope.stage.move_rel([0, 0, step_size])
if return_to_start:
- logging.debug("Returning to {}".format(initial_position))
+ logging.debug("Returning to %s", (initial_position))
microscope.stage.move_abs(initial_position)
scan_extension_v2 = ScanExtension()
+class TileScanArgs(FullCaptureArgs):
+ namemode = fields.String(missing="coordinates", example="coordinates")
+ grid = fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3])
+ style = fields.String(missing="raster")
+ autofocus_dz = fields.Integer(missing=50)
+ fast_autofocus = fields.Boolean(missing=False)
+ stride_size = fields.List(
+ fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
+ )
+
+
class TileScanAPI(ActionView):
- args = {
- "filename": fields.String(missing=None, example=None),
- "namemode": fields.String(missing="coordinates", example="coordinates"),
- "temporary": fields.Boolean(missing=False),
- "stride_size": fields.List(
- fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
- ),
- "grid": fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]),
- "style": fields.String(missing="raster"),
- "autofocus_dz": fields.Integer(missing=50),
- "fast_autofocus": fields.Boolean(missing=False),
- "use_video_port": fields.Boolean(missing=False),
- "bayer": fields.Boolean(missing=False),
- "annotations": fields.Dict(missing={}, example={"Foo": "Bar"}),
- "tags": fields.List(fields.String, missing=[]),
- "resize": fields.Dict(missing=None), # TODO: Validate keys
- }
+ args = TileScanArgs()
# Allow 10 seconds to stop upon DELETE request
# Gives fast-autofocus time to finish if it's running
diff --git a/openflexure_microscope/api/default_extensions/zip_builder.py b/openflexure_microscope/api/default_extensions/zip_builder.py
index 42470901..5db25a70 100644
--- a/openflexure_microscope/api/default_extensions/zip_builder.py
+++ b/openflexure_microscope/api/default_extensions/zip_builder.py
@@ -11,8 +11,6 @@ from labthings.schema import Schema, pre_dump
from labthings.utilities import description_from_view
from labthings.views import ActionView, PropertyView, View
-from openflexure_microscope.devel import JsonResponse, request
-
class ZipObjectSchema(Schema):
id = fields.String()
@@ -128,12 +126,15 @@ default_zip_manager = ZipManager()
class ZipBuilderAPIView(ActionView):
- def post(self):
- ids = list(JsonResponse(request).json)
+ args = fields.List(fields.String(), required=True)
+
+ def post(self, args):
microscope = find_component("org.openflexure.microscope")
# Return a handle on the autofocus task
- return default_zip_manager.marshaled_build_zip_from_capture_ids(microscope, ids)
+ return default_zip_manager.marshaled_build_zip_from_capture_ids(
+ microscope, args
+ )
class ZipListAPIView(PropertyView):
diff --git a/openflexure_microscope/api/static/src/App.vue b/openflexure_microscope/api/static/src/App.vue
index 09ccbb4f..320b0384 100644
--- a/openflexure_microscope/api/static/src/App.vue
+++ b/openflexure_microscope/api/static/src/App.vue
@@ -184,7 +184,6 @@ export default {
this.checkConnection();
// Handle guided tour
// If the user has already completed or skipped the guided tour
- // TODO: Only run this if connected to the API
var completedTour = this.getLocalStorageObj("completedTour") || false;
if (!completedTour) {
this.$tours["guidedTour"].start();
@@ -207,8 +206,6 @@ export default {
);
// Keyboard shortcuts
-
- // TODO: Shortcut guide
Mousetrap.bind("?", () => {
console.log(this.keyboardManual);
this.toggleModalElement(this.$refs["keyboardManualModal"]); // Calls the mixin
diff --git a/openflexure_microscope/api/static/src/components/appContent.vue b/openflexure_microscope/api/static/src/components/appContent.vue
index 51e3cd0f..6410db42 100644
--- a/openflexure_microscope/api/static/src/components/appContent.vue
+++ b/openflexure_microscope/api/static/src/components/appContent.vue
@@ -16,63 +16,27 @@
id="switcher-left"
class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1 uk-text-center"
>
-
- visibility
-
-
-
- photo_library
-
-
-
-
-
- gamepad
-
-
- camera_alt
-
-
-
-
-
+
+
+
- settings_overscan
+ {{ item.icon }}
+
+
+
-
- settings
-
-
-
- assignment_late
-
-
-
- info
-
+
+
+
+
+ {{ item.icon }}
+
+
+
+
@@ -126,69 +77,19 @@
id="container-left"
class="uk-padding-remove uk-height-1-1 uk-width-expand"
>
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -252,14 +165,69 @@ export default {
return {
plugins: [],
currentTab: "view",
- unwatchStoreFunction: null
+ unwatchStoreFunction: null,
+ topTabs: [
+ {
+ id: "view",
+ icon: "visibility",
+ component: viewContent
+ },
+ {
+ id: "gallery",
+ icon: "photo_library",
+ component: galleryContent,
+ divide: true // Add a divider after this tab icon
+ },
+ {
+ id: "navigate",
+ icon: "gamepad",
+ component: navigateContent
+ },
+ {
+ id: "capture",
+ icon: "camera_alt",
+ component: captureContent,
+ divide: true // Add a divider after this tab icon
+ }
+ ],
+ bottomTabs: [
+ {
+ id: "settings",
+ icon: "settings",
+ component: settingsContent,
+ class: "uk-margin-auto-top"
+ },
+ {
+ id: "logging",
+ icon: "assignment_late",
+ component: loggingContent
+ },
+ {
+ id: "about",
+ icon: "info",
+ component: aboutContent
+ }
+ ]
};
},
computed: {
+ enabledTopTabs: function() {
+ var enabledTabs = this.topTabs;
+ if (this.$store.state.globalSettings.IHIEnabled) {
+ enabledTabs.push({
+ id: "slidescan",
+ icon: "settings_overscan",
+ component: slideScanContent
+ });
+ }
+ return enabledTabs;
+ },
+
pluginsUri: function() {
return `${this.$store.getters.baseUri}/api/v2/extensions`;
},
+
pluginsGuiList: function() {
// List of plugin GUIs, obtained from this.plugins values
console.log("Recalculating plugins");
@@ -271,15 +239,21 @@ export default {
}
return pluginGuis;
},
+
tabOrder: function() {
- // TODO: There must be a better way to do this, somehow reading the order of the HTML elements?
- var ind = ["view", "gallery", "navigate", "capture", "settings"];
+ var ind = [];
+ for (const tab of this.enabledTopTabs) {
+ ind.push(tab.id);
+ }
for (const plugin of this.pluginsGuiList) {
ind.push(plugin.id);
}
- ind.push("about");
+ for (const tab of this.bottomTabs) {
+ ind.push(tab.id);
+ }
return ind;
},
+
currentTabIndex: function() {
return this.tabOrder.indexOf(this.currentTab);
}
diff --git a/openflexure_microscope/api/static/src/components/pluginComponents/JsonForm.vue b/openflexure_microscope/api/static/src/components/pluginComponents/JsonForm.vue
index 70262bd8..d53e9eb1 100644
--- a/openflexure_microscope/api/static/src/components/pluginComponents/JsonForm.vue
+++ b/openflexure_microscope/api/static/src/components/pluginComponents/JsonForm.vue
@@ -131,7 +131,6 @@ export default {
},
submitApiUri: function() {
- // TODO: This could probably be handled more explicitally
return this.pluginApiUri + this.route;
}
},
diff --git a/openflexure_microscope/api/utilities/__init__.py b/openflexure_microscope/api/utilities/__init__.py
index 6e01e9dc..7c12b2a9 100644
--- a/openflexure_microscope/api/utilities/__init__.py
+++ b/openflexure_microscope/api/utilities/__init__.py
@@ -24,54 +24,6 @@ def blueprint_name_for_module(module_name, api_ver=2, suffix=""):
return f"v{api_ver}_{bp_name}_blueprint{suffix}"
-class JsonResponse:
- def __init__(self, request):
- """
- Object to wrap up simple functionality for parsing a JSON response.
- """
- # Try to load as json
- try:
- self.json = (
- request.get_json()
- ) #: dict: Dictionary representation of request JSON
- # If malformed JSON is passed, make an empty dictionary
- except BadRequest as e:
- logging.error(e)
- self.json = {}
-
- if self.json is None:
- self.json = {}
-
- # Store raw response data
- self.data = request.get_data() #: str: String representation of request data
-
- if self.data is None:
- self.data = ""
-
- def param(self, key, default=None, convert=None):
- """
- Check if a key exists in a JSON/dictionary payload, and returns it.
-
- Args:
- key (str): JSON key to look for
- default: Value to return if no matching key-value pair is found.
- convert: Converter function. By passing a type, the value will be converted to that type.
- **Use with caution!**
- """
- # If no JSON payload exists, make an empty dictionary
- if not self.json:
- self.json = {}
-
- if key in self.json:
- val = self.json[key]
- else:
- val = default
-
- if convert and (val is not None):
- val = convert(val)
- return val
-
-
def gen(camera):
"""Video streaming generator function."""
while True:
@@ -121,12 +73,10 @@ def init_default_extensions(extension_dir):
default_ext_path = os.path.join(extension_dir, "defaults.py")
if not os.path.isfile(default_ext_path): # If user extensions file doesn't exist
- logging.warning(
- "No extension file found at {}. Creating...".format(extension_dir)
- )
+ logging.warning("No extension file found at %s. Creating...", (extension_dir))
create_file(default_ext_path)
- logging.info("Populating {}...".format(default_ext_path))
+ logging.info("Populating %s...", (default_ext_path))
with open(default_ext_path, "w") as outfile:
outfile.write(_DEFAULT_EXTENSION_INIT)
diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py
index 02d9b229..7c1f1fee 100644
--- a/openflexure_microscope/api/v2/views/actions/camera.py
+++ b/openflexure_microscope/api/v2/views/actions/camera.py
@@ -1,33 +1,39 @@
import io
import logging
-from flask import abort, send_file
-from labthings import fields, find_component
+from flask import send_file
+from labthings import Schema, fields, find_component
from labthings.views import ActionView
from openflexure_microscope.api.v2.views.captures import CaptureSchema
+class CaptureResizeSchema(Schema):
+ width = fields.Integer(example=640, required=True)
+ height = fields.Integer(example=480, required=True)
+
+
+class BasicCaptureArgs(Schema):
+ use_video_port = fields.Boolean(missing=False)
+ bayer = fields.Boolean(
+ missing=False, description="Include raw bayer data in capture"
+ )
+ resize = fields.Nested(CaptureResizeSchema(), required=False)
+
+
+class FullCaptureArgs(BasicCaptureArgs):
+ filename = fields.String(example="MyFileName")
+ temporary = fields.Boolean(missing=False, description="Delete capture on shutdown")
+ annotations = fields.Dict(missing={}, example={"Client": "SwaggerUI"})
+ tags = fields.List(fields.String, missing=[], example=["docs"])
+
+
class CaptureAPI(ActionView):
"""
Create a new image capture.
"""
- args = {
- "filename": fields.String(example="MyFileName"),
- "temporary": fields.Boolean(
- missing=False, description="Delete capture on shutdown"
- ),
- "use_video_port": fields.Boolean(missing=False),
- "bayer": fields.Boolean(
- missing=False, description="Store raw bayer data in file"
- ),
- "annotations": fields.Dict(missing={}, example={"Client": "SwaggerUI"}),
- "tags": fields.List(fields.String, missing=[], example=["docs"]),
- "resize": fields.Dict(
- missing=None, example={"width": 640, "height": 480}
- ), # TODO: Validate keys
- }
+ args = FullCaptureArgs()
schema = CaptureSchema()
def post(self, args):
@@ -38,13 +44,10 @@ class CaptureAPI(ActionView):
resize = args.get("resize", None)
if resize:
- if ("width" in resize) and ("height" in resize):
- resize = (
- int(resize["width"]),
- int(resize["height"]),
- ) # Convert dict to tuple
- else:
- abort(404)
+ resize = (
+ int(resize["width"]),
+ int(resize["height"]),
+ ) # Convert dict to tuple
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
with microscope.camera.lock:
@@ -64,16 +67,7 @@ class RAMCaptureAPI(ActionView):
Take a non-persistant image capture.
"""
- args = {
- "use_video_port": fields.Boolean(missing=True),
- "bayer": fields.Boolean(
- missing=False, description="Return with raw bayer data"
- ),
- "resize": fields.Dict(
- missing=None, example={"width": 640, "height": 480}
- ), # TODO: Validate keys
- }
-
+ args = BasicCaptureArgs()
responses = {200: {"content_type": "image/jpeg"}}
def post(self, args):
@@ -84,13 +78,10 @@ class RAMCaptureAPI(ActionView):
resize = args.get("resize", None)
if resize:
- if ("width" in resize) and ("height" in resize):
- resize = (
- int(resize["width"]),
- int(resize["height"]),
- ) # Convert dict to tuple
- else:
- abort(404)
+ resize = (
+ int(resize["width"]),
+ int(resize["height"]),
+ ) # Convert dict to tuple
# Open a BytesIO stream to be destroyed once request has returned
with microscope.camera.lock, io.BytesIO() as stream:
@@ -133,8 +124,6 @@ class GPUPreviewStartAPI(ActionView):
window = [int(w) for w in window]
microscope.camera.start_preview(fullscreen=fullscreen, window=window)
-
- # TODO: Make schema for microscope state
return microscope.state
@@ -145,5 +134,4 @@ class GPUPreviewStopAPI(ActionView):
"""
microscope = find_component("org.openflexure.microscope")
microscope.camera.stop_preview()
- # TODO: Make schema for microscope state
return microscope.state
diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py
index cd989f5c..53e0b0b1 100644
--- a/openflexure_microscope/api/v2/views/actions/stage.py
+++ b/openflexure_microscope/api/v2/views/actions/stage.py
@@ -25,11 +25,11 @@ class MoveStageAPI(ActionView):
# Handle absolute positioning (calculate a relative move from current position and target)
if (args.get("absolute")) and (microscope.stage): # Only if stage exists
target_position = axes_to_array(args, ["x", "y", "z"])
- logging.debug("TARGET: {}".format(target_position))
+ logging.debug("TARGET: %s", (target_position))
position = [
target_position[i] - microscope.stage.position[i] for i in range(3)
]
- logging.debug("DELTA: {}".format(position))
+ logging.debug("DELTA: %s", (position))
else:
# Get coordinates from payload
@@ -45,7 +45,6 @@ class MoveStageAPI(ActionView):
else:
logging.warning("Unable to move. No stage found.")
- # TODO: Make schema for microscope state
return microscope.state["stage"]["position"]
@@ -60,5 +59,4 @@ class ZeroStageAPI(ActionView):
with microscope.stage.lock(timeout=1):
microscope.stage.zero_position()
- # TODO: Make schema for microscope state
return microscope.state["stage"]
diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py
index 9272aafb..9de19506 100644
--- a/openflexure_microscope/api/v2/views/captures.py
+++ b/openflexure_microscope/api/v2/views/captures.py
@@ -1,13 +1,13 @@
-import logging
-
from flask import abort, redirect, request, send_file, url_for
from labthings import Schema, fields, find_component
-from labthings.marshalling import marshal_with
+from labthings.marshalling import marshal_with, use_args
from labthings.utilities import description_from_view
from labthings.views import PropertyView, View
from marshmallow import pre_dump
-from openflexure_microscope.api.utilities import JsonResponse, get_bool
+from openflexure_microscope.api.utilities import get_bool
+
+# SCHEMAS
class InstrumentSchema(Schema):
@@ -23,17 +23,25 @@ class ImageSchema(Schema):
format = fields.String()
name = fields.String()
tags = fields.List(fields.String())
- annotations = fields.Dict()
+ annotations = fields.Dict(keys=fields.Str(), values=fields.Str())
class CaptureMetadataSchema(Schema):
- experimenter = fields.Dict() # TODO: Make schema
- experimenterGroup = fields.Dict() # TODO: Make schema
- dataset = fields.Dict() # TODO: Make schema
+ # Full dataset dictionary will change depending on the type of
+ # dataset, so we can't make a specific schema in this case.
+ dataset = fields.Dict()
+ # Nested schema for Image data
image = fields.Nested(ImageSchema())
+ # Nested schema for instrument data
instrument = fields.Nested(InstrumentSchema())
+class BasicDatasetSchema(Schema):
+ id = fields.UUID()
+ name = fields.String()
+ type = fields.String()
+
+
class CaptureSchema(ImageSchema):
"""
Schema containing only basic attributes required
@@ -41,10 +49,15 @@ class CaptureSchema(ImageSchema):
are returned by using FullCaptureSchema
"""
- dataset = fields.Dict() # TODO: Make schema
+ # We need dataset information in the capture array
+ # so that client applications can sort data into folders
+ # without the server having to do a tonne of file IO
+ dataset = fields.Nested(BasicDatasetSchema())
file = fields.String(
data_key="path", description="Path of file on microscope device"
)
+ # No need to make a schema for links as we only ever
+ # create the dictionary right here in `generate_links`
links = fields.Dict()
@pre_dump
@@ -104,6 +117,9 @@ class FullCaptureSchema(CaptureSchema):
metadata = fields.Nested(CaptureMetadataSchema())
+# VIEWS
+
+
class CaptureList(PropertyView):
tags = ["captures"]
schema = CaptureSchema(many=True)
@@ -203,7 +219,8 @@ class CaptureTags(View):
return capture_obj.tags
- def put(self, id_):
+ @use_args(fields.List(fields.String(), required=True))
+ def put(self, args, id_):
"""
Add tags to a single image capture
"""
@@ -213,17 +230,12 @@ class CaptureTags(View):
if not capture_obj:
return abort(404) # 404 Not Found
- # TODO: Replace with normal Flask request JSON thing
- data_dict = JsonResponse(request).json
-
- if type(data_dict) != list:
- return abort(400)
-
- capture_obj.put_tags(data_dict)
+ capture_obj.put_tags(args)
return capture_obj.tags
- def delete(self, id_):
+ @use_args(fields.List(fields.String(), required=True))
+ def delete(self, args, id_):
"""
Delete tags from a single image capture
"""
@@ -233,12 +245,7 @@ class CaptureTags(View):
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:
+ for tag in args:
capture_obj.delete_tag(str(tag))
return capture_obj.tags
@@ -259,7 +266,8 @@ class CaptureAnnotations(View):
return capture_obj.annotations
- def put(self, id_):
+ @use_args(fields.Dict())
+ def put(self, args, id_):
"""
Update metadata for a single image capture
"""
@@ -269,12 +277,6 @@ class CaptureAnnotations(View):
if not capture_obj:
return abort(404) # 404 Not Found
- data_dict = JsonResponse(request).json
- logging.debug(data_dict)
-
- if type(data_dict) != dict:
- return abort(400)
-
- capture_obj.put_annotations(data_dict)
+ capture_obj.put_annotations(args)
return capture_obj.annotations
diff --git a/openflexure_microscope/api/v2/views/instrument.py b/openflexure_microscope/api/v2/views/instrument.py
index 000bfb6f..c441240e 100644
--- a/openflexure_microscope/api/v2/views/instrument.py
+++ b/openflexure_microscope/api/v2/views/instrument.py
@@ -1,12 +1,11 @@
import logging
-from flask import abort, request
-from labthings import find_component
+from flask import abort
+from labthings import fields, find_component
+from labthings.marshalling import use_args
from labthings.utilities import create_from_path, get_by_path, set_by_path
from labthings.views import PropertyView, View
-from openflexure_microscope.api.utilities import JsonResponse
-
class SettingsProperty(PropertyView):
def get(self):
@@ -16,17 +15,17 @@ class SettingsProperty(PropertyView):
microscope = find_component("org.openflexure.microscope")
return microscope.read_settings()
- def put(self):
+ @use_args(fields.Dict())
+ def put(self, args):
"""
Update current microscope settings, including camera and stage
"""
microscope = find_component("org.openflexure.microscope")
- payload = JsonResponse(request)
logging.debug("Updating settings from PUT request:")
- logging.debug(payload.json)
+ logging.debug(args)
- microscope.update_settings(payload.json)
+ microscope.update_settings(args)
microscope.save_settings()
return self.get()
@@ -50,16 +49,16 @@ class NestedSettingsProperty(View):
return value
- def put(self, route):
+ @use_args(fields.Dict())
+ def put(self, args, route):
"""
Update a nested section of the current microscope settings
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
- payload = JsonResponse(request)
dictionary = create_from_path(keys)
- set_by_path(dictionary, keys, payload.json)
+ set_by_path(dictionary, keys, args)
microscope.update_settings(dictionary)
microscope.save_settings()
diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py
index de1530a3..dab2b06a 100644
--- a/openflexure_microscope/camera/base.py
+++ b/openflexure_microscope/camera/base.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
+import io
import logging
import threading
import time
-import io
from abc import ABCMeta, abstractmethod
from collections import namedtuple
@@ -113,10 +113,10 @@ class BaseCamera(metaclass=ABCMeta):
def close(self):
"""Close the BaseCamera and all attached StreamObjects."""
- logging.info("Closing {}".format(self))
+ logging.info("Closing %s", (self))
# Stop worker thread
self.stop_worker()
- logging.info("Closed {}".format(self))
+ logging.info("Closed %s", (self))
# START AND STOP WORKER THREAD
diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py
index 11dafab8..4d47e2de 100644
--- a/openflexure_microscope/camera/pi.py
+++ b/openflexure_microscope/camera/pi.py
@@ -156,10 +156,10 @@ class PiCameraStreamer(BaseCamera):
for key in PiCameraStreamer.picamera_settings_keys:
try:
value = getattr(self.camera, key)
- logging.debug("Reading PiCamera().{}: {}".format(key, value))
+ logging.debug("Reading PiCamera().%s: %s", key, value)
conf_dict["picamera"][key] = value
except AttributeError:
- logging.debug("Unable to read PiCamera attribute {}".format(key))
+ logging.debug("Unable to read PiCamera attribute %s", (key))
# Include a serialised lens shading table
if (
@@ -243,26 +243,22 @@ class PiCameraStreamer(BaseCamera):
# Set exposure mode
if "exposure_mode" in settings_dict:
logging.debug(
- "Applying exposure_mode: {}".format(settings_dict["exposure_mode"])
+ "Applying exposure_mode: %s", (settings_dict["exposure_mode"])
)
self.camera.exposure_mode = settings_dict["exposure_mode"]
# Apply gains and let them settle
if "analog_gain" in settings_dict:
- logging.debug(
- "Applying analog_gain: {}".format(settings_dict["analog_gain"])
- )
+ logging.debug("Applying analog_gain: %s", (settings_dict["analog_gain"]))
set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
if "digital_gain" in settings_dict:
- logging.debug(
- "Applying digital_gain: {}".format(settings_dict["digital_gain"])
- )
+ logging.debug("Applying digital_gain: %s", (settings_dict["digital_gain"]))
set_digital_gain(self.camera, float(settings_dict["digital_gain"]))
# Apply shutter speed
if "shutter_speed" in settings_dict:
logging.debug(
- "Applying shutter_speed: {}".format(settings_dict["shutter_speed"])
+ "Applying shutter_speed: %s", (settings_dict["shutter_speed"])
)
self.camera.shutter_speed = int(settings_dict["shutter_speed"])
@@ -272,17 +268,17 @@ class PiCameraStreamer(BaseCamera):
if "awb_gains" in settings_dict:
logging.debug("Applying awb_mode: off")
self.camera.awb_mode = "off"
- logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"]))
+ logging.debug("Applying awb_gains: %s", (settings_dict["awb_gains"]))
self.camera.awb_gains = settings_dict["awb_gains"]
elif "awb_mode" in settings_dict:
- logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"]))
+ logging.debug("Applying awb_mode: %s", (settings_dict["awb_mode"]))
self.camera.awb_mode = settings_dict["awb_mode"]
# Handle some properties that can be quickly applied
batched_keys = ["framerate", "saturation"]
for key in batched_keys:
if (key in settings_dict) and hasattr(self.camera, key):
- logging.debug("Applying {}: {}".format(key, settings_dict[key]))
+ logging.debug("Applying %s: %s", key, settings_dict[key])
setattr(self.camera, key, settings_dict[key])
# Final optional pause to settle
@@ -307,7 +303,7 @@ class PiCameraStreamer(BaseCamera):
for i in range(2):
if np.abs(centre[i] - 0.5) + size / 2 > 0.5:
centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5)
- logging.info("setting zoom, centre {}, size {}".format(centre, size))
+ logging.info("setting zoom, centre %s, size %s", centre, size)
new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size)
self.camera.zoom = new_fov
@@ -329,13 +325,12 @@ class PiCameraStreamer(BaseCamera):
self.preview_active = True
except picamerax.exc.PiCameraMMALError as e:
logging.error(
- "Suppressed a MMALError in start_preview. Exception: {}".format(e)
+ "Suppressed a MMALError in start_preview. Exception: %s", (e)
)
except picamerax.exc.PiCameraValueError as e:
logging.error(
- "Suppressed a ValueError exception in start_preview. Exception: {}".format(
- e
- )
+ "Suppressed a ValueError exception in start_preview. Exception: %s",
+ (e),
)
def stop_preview(self):
@@ -364,7 +359,7 @@ class PiCameraStreamer(BaseCamera):
if not self.record_active:
# Start the camera video recording on port 2
- logging.info("Recording to {}".format(output))
+ logging.info("Recording to %s", (output))
self.camera.start_recording(
output,
@@ -413,12 +408,12 @@ class PiCameraStreamer(BaseCamera):
try:
self.camera.stop_recording(splitter_port=splitter_port)
except picamerax.exc.PiCameraNotRecording:
- logging.info("Not recording on splitter_port {}".format(splitter_port))
+ logging.info("Not recording on splitter_port %s", (splitter_port))
else:
logging.info(
- "Stopped MJPEG stream on port {1}. Switching to {0}.".format(
- self.image_resolution, splitter_port
- )
+ "Stopped MJPEG stream on port %s. Switching to %s.",
+ splitter_port,
+ self.image_resolution,
)
# Increase the resolution for taking an image
@@ -470,9 +465,9 @@ class PiCameraStreamer(BaseCamera):
)
else:
logging.debug(
- "Started MJPEG stream at {} on port {}".format(
- self.stream_resolution, splitter_port
- )
+ "Started MJPEG stream at %s on port %s",
+ self.stream_resolution,
+ splitter_port,
)
def capture(
@@ -501,7 +496,7 @@ class PiCameraStreamer(BaseCamera):
output_object (str/BytesIO): Target object.
"""
with self.lock:
- logging.info("Capturing to {}".format(output))
+ logging.info("Capturing to %s", (output))
# Set resolution and stop stream recording if necessary
if not use_video_port:
@@ -537,7 +532,7 @@ class PiCameraStreamer(BaseCamera):
logging.debug("Creating PiRGBArray")
with picamerax.array.PiRGBArray(self.camera) as output:
- logging.info("Capturing to {}".format(output))
+ logging.info("Capturing to %s", (output))
self.camera.capture(output, format="rgb", use_video_port=True)
diff --git a/openflexure_microscope/camera/set_picamera_gain.py b/openflexure_microscope/camera/set_picamera_gain.py
index d1d7175a..fc04f851 100644
--- a/openflexure_microscope/camera/set_picamera_gain.py
+++ b/openflexure_microscope/camera/set_picamera_gain.py
@@ -55,26 +55,17 @@ if __name__ == "__main__":
# fix the shutter speed
cam.shutter_speed = cam.exposure_speed
- logging.info(
- "Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)
- )
+ logging.info("Current a/d gains: %s, %s", cam.analog_gain, cam.digital_gain)
logging.info("Attempting to set analogue gain to 1")
set_analog_gain(cam, 1)
logging.info("Attempting to set digital gain to 1")
set_digital_gain(cam, 1)
- # The old code is left in here in case it is a useful example...
- # ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
- # MMAL_PARAMETER_DIGITAL_GAIN,
- # to_rational(1))
- # print("Return code: {}".format(ret))
try:
while True:
logging.info(
- "Current a/d gains: {}, {}".format(
- cam.analog_gain, cam.digital_gain
- )
+ "Current a/d gains: %s, %s", cam.analog_gain, cam.digital_gain
)
time.sleep(1)
except KeyboardInterrupt:
diff --git a/openflexure_microscope/captures/__init__.py b/openflexure_microscope/captures/__init__.py
index 4df0ac64..7d15ca40 100644
--- a/openflexure_microscope/captures/__init__.py
+++ b/openflexure_microscope/captures/__init__.py
@@ -1,3 +1,3 @@
from . import capture, capture_manager
-from .capture import CaptureObject, THUMBNAIL_SIZE
+from .capture import THUMBNAIL_SIZE, CaptureObject
from .capture_manager import CaptureManager
diff --git a/openflexure_microscope/captures/capture.py b/openflexure_microscope/captures/capture.py
index fcddc98f..36d252b4 100644
--- a/openflexure_microscope/captures/capture.py
+++ b/openflexure_microscope/captures/capture.py
@@ -6,13 +6,13 @@ import logging
import os
import uuid
from collections import OrderedDict
-from PIL import Image
import dateutil.parser
+from PIL import Image
-from openflexure_microscope.camera import piexif
-from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
-from openflexure_microscope.config import JSONEncoder
+from openflexure_microscope.captures import piexif
+from openflexure_microscope.captures.piexif import InvalidImageDataError
+from openflexure_microscope.json import JSONEncoder
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
THUMBNAIL_SIZE = (200, 150)
@@ -25,23 +25,23 @@ def make_file_list(directory, formats):
glob.glob("{}/**/*.{}".format(directory, fmt.lower()), recursive=True)
)
- logging.info("{} capture files found on disk".format(len(files)))
+ logging.info("%s capture files found on disk", (len(files)))
return files
def build_captures_from_exif(capture_path):
- logging.debug("Reloading captures from {}...".format(capture_path))
+ logging.debug("Reloading captures from %s...", (capture_path))
files = make_file_list(capture_path, EXIF_FORMATS)
captures = OrderedDict()
for f in files:
- logging.debug("Reloading capture {}...".format(f))
+ logging.debug("Reloading capture %s...", (f))
capture = capture_from_path(f)
if capture:
captures[capture.id] = capture
- logging.info("{} capture files successfully reloaded".format(len(captures)))
+ logging.info("%s capture files successfully reloaded", (len(captures)))
return captures
@@ -58,7 +58,7 @@ def capture_from_path(path):
capture.sync_basic_metadata()
return capture
except (InvalidImageDataError, json.decoder.JSONDecodeError):
- logging.error("Invalid metadata at {}.".format(path))
+ logging.error("Invalid metadata at %s.", (path))
return None
@@ -75,7 +75,7 @@ class CaptureObject(object):
# Store a nice ID
self.id = uuid.uuid4() #: str: Unique capture ID
- logging.debug("Created CaptureObject {}".format(self.id))
+ logging.debug("Created CaptureObject %s", (self.id))
self.time = datetime.datetime.now()
@@ -316,7 +316,7 @@ class CaptureObject(object):
"""
if self.exists: # If data file exists
- logging.info("Opening from file {}".format(self.file))
+ logging.info("Opening from file %s", (self.file))
with open(self.file, "rb") as f:
d = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream
@@ -364,7 +364,7 @@ class CaptureObject(object):
"""If the StreamObject has been saved, delete the file."""
if os.path.isfile(self.file):
- logging.info("Deleting file {}".format(self.file))
+ logging.info("Deleting file %s", (self.file))
os.remove(self.file)
return True
diff --git a/openflexure_microscope/captures/capture_manager.py b/openflexure_microscope/captures/capture_manager.py
index e5e290eb..d636f7a4 100644
--- a/openflexure_microscope/captures/capture_manager.py
+++ b/openflexure_microscope/captures/capture_manager.py
@@ -61,7 +61,7 @@ class CaptureManager:
self.close()
def close(self):
- logging.info("Closing {}".format(self))
+ logging.info("Closing %s", (self))
# Close all StreamObjects
for capture_list in [self.images.values(), self.videos.values()]:
for stream_object in capture_list:
@@ -75,9 +75,9 @@ class CaptureManager:
"""
if os.path.isdir(self.paths["temp"]):
- logging.info("Clearing {}...".format(self.paths["temp"]))
+ logging.info("Clearing %s...", (self.paths["temp"]))
shutil.rmtree(self.paths["temp"])
- logging.debug("Cleared {}.".format(self.paths["temp"]))
+ logging.debug("Cleared %s.", (self.paths["temp"]))
def rebuild_captures(self):
self.images = build_captures_from_exif(self.paths["default"])
@@ -118,25 +118,7 @@ class CaptureManager:
# CREATING NEW CAPTURES
- def new_image(
- self,
- temporary: bool = True,
- filename: str = None,
- folder: str = "",
- fmt: str = "jpeg",
- ):
-
- """
- Create a new image capture object.
-
- Args:
- temporary (bool): Should the data be deleted after session ends.
- Creating the capture with a content manager sets this to true.
- filename (str): Name of the stored file. Defaults to timestamp.
- folder (str): Name of the folder in which to store the capture.
- fmt (str): Format of the capture.
- """
-
+ def _new_output(self, temporary, filename, folder, fmt):
# Generate file name
if not filename:
filename = generate_numbered_basename(self.images.values())
@@ -156,10 +138,32 @@ class CaptureManager:
if temporary:
output.put_tags(["temporary"])
+ return output
+
+ def new_image(
+ self,
+ temporary: bool = True,
+ filename: str = None,
+ folder: str = "",
+ fmt: str = "jpeg",
+ ):
+
+ """
+ Create a new image capture object.
+
+ Args:
+ temporary (bool): Should the data be deleted after session ends.
+ Creating the capture with a content manager sets this to true.
+ filename (str): Name of the stored file. Defaults to timestamp.
+ folder (str): Name of the folder in which to store the capture.
+ fmt (str): Format of the capture.
+ """
+ # Create a new output object
+ output = self._new_output(temporary, filename, folder, fmt)
+
# Update capture list
capture_key = str(output.id)
logging.debug("Adding image %s with key %s", output, capture_key)
-
self.images[capture_key] = output
return output
@@ -182,26 +186,8 @@ class CaptureManager:
folder (str): Name of the folder in which to store the capture.
fmt (str): Format of the capture.
"""
- # TODO: Remove the redundancy here
-
- # Generate file name
- if not filename:
- filename = generate_numbered_basename(self.videos.values())
- logging.debug(filename)
- filename = "{}.{}".format(filename, fmt)
-
- # Generate folder
- base_folder = self.paths["temp"] if temporary else self.paths["default"]
- folder = os.path.join(base_folder, folder)
-
- # Generate file path
- filepath = os.path.join(folder, filename)
-
- # Create capture object
- output = CaptureObject(filepath=filepath)
- # Insert a temporary tag if temporary
- if temporary:
- output.put_tags(["temporary"])
+ # Create a new output object
+ output = self._new_output(temporary, filename, folder, fmt)
# Update capture list
capture_key = str(output.id)
diff --git a/openflexure_microscope/camera/piexif/LICENSE.txt b/openflexure_microscope/captures/piexif/LICENSE.txt
similarity index 100%
rename from openflexure_microscope/camera/piexif/LICENSE.txt
rename to openflexure_microscope/captures/piexif/LICENSE.txt
diff --git a/openflexure_microscope/camera/piexif/__init__.py b/openflexure_microscope/captures/piexif/__init__.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/__init__.py
rename to openflexure_microscope/captures/piexif/__init__.py
diff --git a/openflexure_microscope/camera/piexif/_common.py b/openflexure_microscope/captures/piexif/_common.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_common.py
rename to openflexure_microscope/captures/piexif/_common.py
diff --git a/openflexure_microscope/camera/piexif/_dump.py b/openflexure_microscope/captures/piexif/_dump.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_dump.py
rename to openflexure_microscope/captures/piexif/_dump.py
diff --git a/openflexure_microscope/camera/piexif/_exceptions.py b/openflexure_microscope/captures/piexif/_exceptions.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_exceptions.py
rename to openflexure_microscope/captures/piexif/_exceptions.py
diff --git a/openflexure_microscope/camera/piexif/_exif.py b/openflexure_microscope/captures/piexif/_exif.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_exif.py
rename to openflexure_microscope/captures/piexif/_exif.py
diff --git a/openflexure_microscope/camera/piexif/_insert.py b/openflexure_microscope/captures/piexif/_insert.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_insert.py
rename to openflexure_microscope/captures/piexif/_insert.py
diff --git a/openflexure_microscope/camera/piexif/_load.py b/openflexure_microscope/captures/piexif/_load.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_load.py
rename to openflexure_microscope/captures/piexif/_load.py
diff --git a/openflexure_microscope/camera/piexif/_remove.py b/openflexure_microscope/captures/piexif/_remove.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_remove.py
rename to openflexure_microscope/captures/piexif/_remove.py
diff --git a/openflexure_microscope/camera/piexif/_transplant.py b/openflexure_microscope/captures/piexif/_transplant.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_transplant.py
rename to openflexure_microscope/captures/piexif/_transplant.py
diff --git a/openflexure_microscope/camera/piexif/_webp.py b/openflexure_microscope/captures/piexif/_webp.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/_webp.py
rename to openflexure_microscope/captures/piexif/_webp.py
diff --git a/openflexure_microscope/camera/piexif/helper.py b/openflexure_microscope/captures/piexif/helper.py
similarity index 100%
rename from openflexure_microscope/camera/piexif/helper.py
rename to openflexure_microscope/captures/piexif/helper.py
diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py
index cfca29f3..5788a385 100644
--- a/openflexure_microscope/config.py
+++ b/openflexure_microscope/config.py
@@ -3,18 +3,9 @@ import json
import logging
import os
import shutil
-from fractions import Fraction
-from uuid import UUID
-import numpy as np
-from labthings.json import LabThingsJSONEncoder
-
-from .paths import (
- CONFIGURATION_FILE_PATH,
- DEFAULT_CONFIGURATION_FILE_PATH,
- DEFAULT_SETTINGS_FILE_PATH,
- SETTINGS_FILE_PATH,
-)
+from .json import JSONEncoder
+from .paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
class OpenflexureSettingsFile:
@@ -33,7 +24,12 @@ class OpenflexureSettingsFile:
self.path = path
# Initialise basic config file with defaults if it doesn't exist
- initialise_file(self.path, populate=defaults)
+ initialise_file(
+ self.path,
+ # Populate with default dictionary, or empty JSON if empty
+ populate=json.dumps(defaults, cls=JSONEncoder, indent=2, sort_keys=True)
+ or "{}\n",
+ )
def load(self) -> dict:
"""
@@ -78,32 +74,6 @@ class OpenflexureSettingsFile:
return settings
-class JSONEncoder(LabThingsJSONEncoder):
- """
- A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
- """
-
- def default(self, o):
- if isinstance(o, UUID):
- return str(o)
- # PiCamera fractions
- elif isinstance(o, Fraction):
- return float(o)
- # Numpy integers
- elif isinstance(o, np.integer):
- return int(o)
- # Numpy arrays
- elif isinstance(o, np.ndarray):
- return o.tolist()
- # UUIDs
- elif isinstance(o, UUID):
- return str(o)
- else:
- # call base class implementation which takes care of
- # raising exceptions for unsupported types
- return LabThingsJSONEncoder.default(self, o)
-
-
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
@@ -116,7 +86,7 @@ def load_json_file(config_path) -> dict:
"""
config_path = os.path.expanduser(config_path)
- logging.info("Loading {}...".format(config_path))
+ logging.info("Loading %s...", config_path)
with open(config_path) as config_file:
try:
@@ -139,7 +109,7 @@ def save_json_file(config_path: str, config_dict: dict):
"""
config_path = os.path.expanduser(config_path)
- logging.info("Saving {}...".format(config_path))
+ logging.info("Saving %s...", config_path)
logging.debug(config_dict)
with open(config_path, "w") as outfile:
@@ -172,33 +142,26 @@ def initialise_file(config_path, populate: str = "{}\n"):
"""
config_path = os.path.expanduser(config_path)
- logging.debug("Initialising {}".format(config_path))
- logging.debug("Exists: {}".format(os.path.exists(config_path)))
+ logging.debug("Initialising %s", (config_path))
+ logging.debug("Exists: %s", (os.path.exists(config_path)))
if not os.path.exists(config_path): # If user config file doesn't exist
- logging.warning("No config file found at {}. Creating...".format(config_path))
+ logging.warning("No config file found at %s. Creating...", (config_path))
create_file(config_path)
- logging.info("Populating {}...".format(config_path))
+ logging.info("Populating %s...", (config_path))
with open(config_path, "w") as outfile:
outfile.write(populate)
-# Load the default settings
-with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
- DEFAULT_SETTINGS = default_settings.read()
-
#: Default user settings object
-user_settings = OpenflexureSettingsFile(
- path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS
-)
-
-
-# Load the default configuration
-with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration:
- DEFAULT_CONFIGURATION = default_configuration.read()
+user_settings = OpenflexureSettingsFile(path=SETTINGS_FILE_PATH)
#: Default user settings object
user_configuration = OpenflexureSettingsFile(
- path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION
+ path=CONFIGURATION_FILE_PATH,
+ defaults={
+ "camera": {"type": "PiCamera"},
+ "stage": {"type": "SangaStage", "port": None},
+ },
)
diff --git a/openflexure_microscope/devel/__init__.py b/openflexure_microscope/devel/__init__.py
index a8e5d39a..fe8d387b 100644
--- a/openflexure_microscope/devel/__init__.py
+++ b/openflexure_microscope/devel/__init__.py
@@ -13,8 +13,6 @@ from labthings import current_action as current_task
from labthings import update_action_data as update_task_data
from labthings import update_action_progress as update_task_progress
-from openflexure_microscope.api.utilities import JsonResponse
-
__all__ = [
"current_task",
"update_task_progress",
diff --git a/openflexure_microscope/json.py b/openflexure_microscope/json.py
new file mode 100644
index 00000000..37b0d7d8
--- /dev/null
+++ b/openflexure_microscope/json.py
@@ -0,0 +1,31 @@
+from fractions import Fraction
+from uuid import UUID
+
+import numpy as np
+from labthings.json import LabThingsJSONEncoder
+
+
+class JSONEncoder(LabThingsJSONEncoder):
+ """
+ A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
+ """
+
+ def default(self, o):
+ if isinstance(o, UUID):
+ return str(o)
+ # PiCamera fractions
+ elif isinstance(o, Fraction):
+ return float(o)
+ # Numpy integers
+ elif isinstance(o, np.integer):
+ return int(o)
+ # Numpy arrays
+ elif isinstance(o, np.ndarray):
+ return o.tolist()
+ # UUIDs
+ elif isinstance(o, UUID):
+ return str(o)
+ else:
+ # call base class implementation which takes care of
+ # raising exceptions for unsupported types
+ return LabThingsJSONEncoder.default(self, o)
diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py
index 17d2be9f..b8b543d0 100644
--- a/openflexure_microscope/microscope.py
+++ b/openflexure_microscope/microscope.py
@@ -10,7 +10,7 @@ import pkg_resources
from expiringdict import ExpiringDict
from openflexure_microscope.camera.mock import MissingCamera
-from openflexure_microscope.captures import CaptureManager, THUMBNAIL_SIZE
+from openflexure_microscope.captures import THUMBNAIL_SIZE, CaptureManager
from openflexure_microscope.stage.mock import MissingStage
from openflexure_microscope.stage.sanga import SangaDeltaStage, SangaStage
@@ -71,7 +71,7 @@ class Microscope:
def close(self):
"""Shut down the microscope hardware."""
- logging.info("Closing {}".format(self))
+ logging.info("Closing %s", (self))
if self.camera:
try:
self.camera.close()
@@ -83,7 +83,7 @@ class Microscope:
except TimeoutError as e:
logging.error(e)
self.captures.close()
- logging.info("Closed {}".format(self))
+ logging.info("Closed %s", (self))
def setup(self, configuration):
"""
@@ -196,32 +196,34 @@ class Microscope:
Applies a settings dictionary to the microscope. Missing parameters will be left untouched.
"""
with self.lock:
- logging.debug("Microscope: Applying settings: {}".format(settings))
+ logging.debug("Microscope: Applying settings: %s", (settings))
# If attached to a camera
if ("camera" in settings) and self.camera:
- self.camera.update_settings(settings.get("camera", {}))
+ self.camera.update_settings(settings.pop("camera", {}))
# If attached to a stage
if ("stage" in settings) and self.stage:
- self.stage.update_settings(settings.get("stage", {}))
+ self.stage.update_settings(settings.pop("stage", {}))
# Capture manager
- self.captures.update_settings(settings.get("captures", {}))
+ self.captures.update_settings(settings.pop("captures", {}))
# Microscope settings
if "id" in settings:
- self.id = settings["id"]
+ self.id = settings.pop("id")
if "name" in settings:
- self.name = settings["name"]
+ self.name = settings.pop("name")
if "fov" in settings:
- self.fov = settings["fov"]
+ self.fov = settings.pop("fov")
# Extension settings
if "extensions" in settings:
- self.extension_settings.update(settings["extensions"])
+ self.extension_settings.update(settings.pop("extensions"))
- # TODO: warn if there are settings that we silently ignore
+ # Warn about any superfluous keys
+ for key in settings.keys():
+ logging.warning("Key %s is unused and was ignored", key)
def read_settings(self, full: bool = True):
"""
diff --git a/openflexure_microscope/microscope_configuration.default.json b/openflexure_microscope/microscope_configuration.default.json
deleted file mode 100644
index b9cbfdce..00000000
--- a/openflexure_microscope/microscope_configuration.default.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "camera": {
- "type": "PiCamera"
- },
- "stage": {
- "type": "SangaStage",
- "port": null
- }
-}
\ No newline at end of file
diff --git a/openflexure_microscope/microscope_settings.default.json b/openflexure_microscope/microscope_settings.default.json
deleted file mode 100644
index 5c408d2e..00000000
--- a/openflexure_microscope/microscope_settings.default.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "fov": [
- 4100,
- 3146
- ],
- "jpeg_quality": 100
-}
\ No newline at end of file
diff --git a/openflexure_microscope/paths.py b/openflexure_microscope/paths.py
index d497873b..38998580 100644
--- a/openflexure_microscope/paths.py
+++ b/openflexure_microscope/paths.py
@@ -39,17 +39,6 @@ def logs_file_path(filename: str):
return os.path.join(logs_dir, filename)
-# HANDLE DEFAULTS FILES STORED IN THIS APPLICATION
-
-HERE = os.path.abspath(os.path.dirname(__file__))
-
-#: Path of default (first-run) microscope settings
-DEFAULT_SETTINGS_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json")
-#: Path of default (first-run) microscope configuration
-DEFAULT_CONFIGURATION_FILE_PATH = os.path.join(
- HERE, "microscope_configuration.default.json"
-)
-
# BASE PATHS
if os.name == "nt":
diff --git a/openflexure_microscope/rescue/auto.py b/openflexure_microscope/rescue/auto.py
index 5bb95f22..9c195113 100644
--- a/openflexure_microscope/rescue/auto.py
+++ b/openflexure_microscope/rescue/auto.py
@@ -1,10 +1,9 @@
import logging
import os
-import sys
import platform
-import pkg_resources
+import sys
-from .error_sources import bcolors
+import pkg_resources
from openflexure_microscope.paths import (
FALLBACK_OPENFLEXURE_VAR_PATH,
@@ -13,11 +12,12 @@ from openflexure_microscope.paths import (
from . import (
check_capture_reload,
- check_settings,
check_picamera,
check_sangaboard,
+ check_settings,
check_system,
)
+from .error_sources import bcolors
# Paths for suggestions
LOGS_PATHS = [
@@ -46,9 +46,7 @@ else:
logger.setLevel(logging.WARNING)
-if __name__ == "__main__":
- spoof = False
-
+def main():
print()
print(bcolors.HEADER + "OpenFlexure Rescue" + bcolors.ENDC)
print()
@@ -88,3 +86,7 @@ if __name__ == "__main__":
print()
for err in error_sources:
print(err.message)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/openflexure_microscope/rescue/check_capture_reload.py b/openflexure_microscope/rescue/check_capture_reload.py
index 54459665..e50ae05b 100644
--- a/openflexure_microscope/rescue/check_capture_reload.py
+++ b/openflexure_microscope/rescue/check_capture_reload.py
@@ -1,9 +1,9 @@
import logging
from openflexure_microscope.captures.capture import (
+ EXIF_FORMATS,
build_captures_from_exif,
make_file_list,
- EXIF_FORMATS,
)
from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH
from openflexure_microscope.config import user_settings
diff --git a/openflexure_microscope/rescue/check_sangaboard.py b/openflexure_microscope/rescue/check_sangaboard.py
index 8c7f6aa7..13b154b1 100644
--- a/openflexure_microscope/rescue/check_sangaboard.py
+++ b/openflexure_microscope/rescue/check_sangaboard.py
@@ -1,7 +1,7 @@
-from .error_sources import ErrorSource
-
from openflexure_microscope.config import user_configuration
+from .error_sources import ErrorSource
+
def main():
error_sources = []
diff --git a/openflexure_microscope/rescue/check_settings.py b/openflexure_microscope/rescue/check_settings.py
index 1c28c9fd..64fffb92 100644
--- a/openflexure_microscope/rescue/check_settings.py
+++ b/openflexure_microscope/rescue/check_settings.py
@@ -1,34 +1,49 @@
import json
import logging
+from .error_sources import ErrorSource
+
ERROR_SOURCES = []
def trace_config_exceptions():
error_sources = []
- from openflexure_microscope.paths import (
- DEFAULT_CONFIGURATION_FILE_PATH,
- SETTINGS_FILE_PATH,
- )
+ from openflexure_microscope.paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
try:
- default_config = json.load(DEFAULT_CONFIGURATION_FILE_PATH)
+ default_config = json.load(CONFIGURATION_FILE_PATH)
if not default_config:
- error_sources.append("default_config_empty")
+ error_sources.append(
+ ErrorSource(
+ "Configuration file is missing or empty. This may occur if the server has never been started."
+ )
+ )
except Exception as e: # pylint: disable=W0703
logging.error("Error parsing config:")
logging.error(e)
- error_sources.append("default_config_error")
+ error_sources.append(
+ ErrorSource(
+ f"Configuration file is malformed. You can reset to the default configuration by deleting {CONFIGURATION_FILE_PATH}."
+ )
+ )
try:
default_settings = json.load(SETTINGS_FILE_PATH)
if not default_settings:
- error_sources.append("default_settings_empty")
+ error_sources.append(
+ ErrorSource(
+ "Settings file is missing or empty. This may occur if the server has never been started."
+ )
+ )
except Exception as e: # pylint: disable=W0703
logging.error("Error parsing settings:")
logging.error(e)
- error_sources.append("default_settings_error")
+ error_sources.append(
+ ErrorSource(
+ f"Settings file is malformed. You can reset to the default settings by deleting {SETTINGS_FILE_PATH}."
+ )
+ )
return error_sources
@@ -39,7 +54,11 @@ def main():
try:
from openflexure_microscope import config as _
except Exception as e: # pylint: disable=W0703
- error_sources.append("config_settings_import_error")
+ error_sources.append(
+ ErrorSource(
+ "Error importing configuration submodule. This could be an error in our code."
+ )
+ )
logging.error("Error importing config:")
logging.error(e)
error_sources.extend(trace_config_exceptions())
diff --git a/openflexure_microscope/rescue/check_system.py b/openflexure_microscope/rescue/check_system.py
index 1c3f31ee..880ba87c 100644
--- a/openflexure_microscope/rescue/check_system.py
+++ b/openflexure_microscope/rescue/check_system.py
@@ -1,5 +1,6 @@
-from urllib.request import urlopen
from urllib.error import URLError
+from urllib.request import urlopen
+
import psutil
from .error_sources import WarningSource
diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py
index 549ab5b9..c3a6c939 100644
--- a/openflexure_microscope/stage/sanga.py
+++ b/openflexure_microscope/stage/sanga.py
@@ -81,7 +81,7 @@ class SangaStage(BaseStage):
@backlash.setter
def backlash(self, blsh):
- logging.debug("Setting backlash to {}".format(blsh))
+ logging.debug("Setting backlash to %s", (blsh))
if blsh is None:
self._backlash = None
elif isinstance(blsh, Iterable):
@@ -262,7 +262,7 @@ class SangaDeltaStage(SangaStage):
# Transform into delta coordinates
displacement = np.dot(self.Tdv, displacement)
- logging.debug("Delta displacement: {}".format(displacement))
+ logging.debug("Delta displacement: %s", (displacement))
# Do the move
SangaStage.move_rel(self, displacement, axis=None, backlash=backlash)
@@ -274,7 +274,7 @@ class SangaDeltaStage(SangaStage):
# Transform into delta coordinates
final = np.dot(self.Tdv, final)
- logging.debug("Delta final: {}".format(final))
+ logging.debug("Delta final: %s", (final))
# Do the move
SangaStage.move_abs(self, final, **kwargs)
diff --git a/poetry.lock b/poetry.lock
index d3b58da7..13771f48 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -79,7 +79,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]]
name = "babel"
-version = "2.8.1"
+version = "2.9.0"
description = "Internationalization utilities"
category = "dev"
optional = false
@@ -283,7 +283,7 @@ i18n = ["Babel (>=0.8)"]
[[package]]
name = "labthings"
-version = "1.1.2"
+version = "1.1.3"
description = "Python implementation of LabThings, based on the Flask microframework"
category = "main"
optional = false
@@ -483,7 +483,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "requests"
-version = "2.24.0"
+version = "2.25.0"
description = "Python HTTP for Humans."
category = "dev"
optional = false
@@ -493,7 +493,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<4"
idna = ">=2.5,<3"
-urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
+urllib3 = ">=1.21.1,<1.27"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
@@ -557,7 +557,7 @@ python-versions = "*"
[[package]]
name = "sphinx"
-version = "3.3.0"
+version = "3.3.1"
description = "Python documentation generator"
category = "dev"
optional = false
@@ -687,7 +687,7 @@ python-versions = "*"
[[package]]
name = "urllib3"
-version = "1.25.11"
+version = "1.26.1"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false
@@ -752,8 +752,8 @@ rpi = ["RPi.GPIO"]
[metadata]
lock-version = "1.1"
-python-versions = "^3.6"
-content-hash = "fc03c76cedfd54d4d6be2ced9eb236dfdd1ffb986fdc731f96ee560053f7aa8a"
+python-versions = "^3.6.1"
+content-hash = "e0aad912d5cb388d3ff395dc8a517f701ba2cbe4c2169771080a889906e91b41"
[metadata.files]
alabaster = [
@@ -781,8 +781,8 @@ attrs = [
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
]
babel = [
- {file = "Babel-2.8.1-py2.py3-none-any.whl", hash = "sha256:be432f50d6c38c705ea45a0c05a4bbb22a70742a93888fbae5e7948da1635d23"},
- {file = "Babel-2.8.1.tar.gz", hash = "sha256:820c195271534e8a86f564ba9ef2c207f356cfeb7e94d2bdc6b57897c4233837"},
+ {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"},
+ {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"},
]
black = [
{file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"},
@@ -855,8 +855,8 @@ jinja2 = [
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
]
labthings = [
- {file = "labthings-1.1.2-py3-none-any.whl", hash = "sha256:1e17e20c3585dbb2293ca1e75c46c4b9a21ee3aa30b71a2d839e16f40c21f600"},
- {file = "labthings-1.1.2.tar.gz", hash = "sha256:0d456f11920c4cd84e94afae2e7f005c3ea65346da5523d51aeec17698808156"},
+ {file = "labthings-1.1.3-py3-none-any.whl", hash = "sha256:51f0bc897fcbacbcbb911658d3daafd41a35373bca4ef2156423cdb728fa3d78"},
+ {file = "labthings-1.1.3.tar.gz", hash = "sha256:3d68c638866f1c525a8b5285570c2d4599005d227b928e78088c0a29e6544007"},
]
lazy-object-proxy = [
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
@@ -1058,8 +1058,8 @@ pyyaml = [
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
]
requests = [
- {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
- {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
+ {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"},
+ {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"},
]
rope = [
{file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"},
@@ -1103,8 +1103,8 @@ snowballstemmer = [
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
]
sphinx = [
- {file = "Sphinx-3.3.0-py3-none-any.whl", hash = "sha256:3abdb2c57a65afaaa4f8573cbabd5465078eb6fd282c1e4f87f006875a7ec0c7"},
- {file = "Sphinx-3.3.0.tar.gz", hash = "sha256:1c21e7c5481a31b531e6cbf59c3292852ccde175b504b00ce2ff0b8f4adc3649"},
+ {file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"},
+ {file = "Sphinx-3.3.1.tar.gz", hash = "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300"},
]
sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
@@ -1162,8 +1162,8 @@ typed-ast = [
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
urllib3 = [
- {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"},
- {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"},
+ {file = "urllib3-1.26.1-py2.py3-none-any.whl", hash = "sha256:61ad24434555a42c0439770462df38b47d05d9e8e353d93ec3742900975e3e65"},
+ {file = "urllib3-1.26.1.tar.gz", hash = "sha256:097116a6f16f13482d2a2e56792088b9b2920f4eb6b4f84a2c90555fb673db74"},
]
webargs = [
{file = "webargs-6.1.1-py2.py3-none-any.whl", hash = "sha256:2ead6ce38559152043ab4ada4d389aef6c804b0c169482e7c92b3f498f690b2c"},
diff --git a/pyproject.toml b/pyproject.toml
index a027c8c2..836e1dfd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,7 @@ packages = [
]
[tool.poetry.dependencies]
-python = "^3.6"
+python = "^3.6.1"
Flask = "^1.0"
numpy = "1.19.2" # Exact version so we can guarantee a wheel
Pillow = "^7.2.0"
@@ -44,13 +44,14 @@ expiringdict = "^1.2.1"
camera-stage-mapping = "0.1.4"
picamerax = ">=20.9.1"
pyyaml = "^5.3.1"
-labthings = "^1.1.2"
+labthings = "^1.1.3"
[tool.poetry.extras]
rpi = ["RPi.GPIO"]
[tool.poetry.scripts]
build_static = 'openflexure_microscope.install:build'
+rescue = 'openflexure_microscope.rescue.auto:main'
[tool.poetry.dev-dependencies]
pynpm = "^0.1.2"
@@ -71,5 +72,5 @@ ensure_newline_before_comments = true
line_length = 88
[tool.pylint.'MESSAGES CONTROL']
-disable = "C0330, C0326, W1202, W0511, C, R"
-max-line-length = 88
\ No newline at end of file
+disable = "fixme,C,R"
+max-line-length = 88
diff --git a/tests/api_client.py b/tests/api_client.py
deleted file mode 100644
index 43ad4942..00000000
--- a/tests/api_client.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from io import BytesIO
-
-import numpy as np
-import requests
-from PIL import Image
-
-
-class APIconnection:
- def __init__(self, host="localhost", port=5000, api_ver="v1"):
- self.base = self.build_base(host=host, port=port, api_ver=api_ver)
-
- def build_base(self, host, port, api_ver):
- return "http://{}:{}/api/{}".format(host, port, api_ver)
-
- def uri(self, suffix):
- return self.base + suffix
-
- def get(self, route, json=True, timeout=5):
- r = requests.get(self.uri(route), timeout=timeout)
- if json:
- return r.json()
- else:
- return r
-
- def post(self, route, json=None, timeout=5):
- r = requests.post(self.uri(route), json=json, timeout=timeout)
- return r.json()
-
- def delete(self, route, timeout=5):
- r = requests.delete(self.uri(route), timeout=timeout)
- return r.json()
-
- def set_overlay(self, message="", size=50):
- json = {"text": message, "size": size}
- return self.post("/camera/overlay", json=json)
-
- def get_overlay(self):
- return self.get("/camera/overlay")
-
- def get_config(self):
- return self.get("/config")
-
- def set_config(self, config_dict):
- return self.post("/config", json=config_dict)
-
- def get_state(self):
- return self.get("/state")
-
- def start_preview(self):
- return self.post("/camera/preview/start")
-
- def stop_preview(self):
- return self.post("/camera/preview/stop")
-
- def move_by(self, x=0, y=0, z=0):
- json = {"x": x, "y": y, "z": z}
- return self.post("/stage/position", json=json)
-
- def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None):
- json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port}
-
- if resize:
- json["size"] = {"width": resize[0], "height": resize[1]}
-
- return self.post("/camera/capture", json=json)
-
- def get_capture(self, capture_id):
- uri_route = "/camera/capture/{}/download".format(capture_id)
- r = self.get(uri_route, json=False)
- img = Image.open(BytesIO(r.content))
- array = np.asarray(img, dtype=np.int32)
- return array
-
- def del_capture(self, capture_id):
- uri_route = "/camera/capture/{}".format(capture_id)
- return self.delete(uri_route)
-
- def capture(
- self,
- use_video_port=True,
- keep_on_disk=False,
- delete_after_use=True,
- resize=None,
- ):
- p = self.new_capture(
- use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize
- )
- capture_id = p["metadata"]["id"]
- img_array = self.get_capture(capture_id)
-
- if delete_after_use:
- self.del_capture(capture_id)
-
- return img_array
-
- def set_zoom(self, zoom_value=1.0):
- json = {"zoom_value": zoom_value}
- return self.post("/camera/zoom", json=json)
-
- def get_zoom(self):
- return self.get("/camera/zoom")
diff --git a/tests/test_api.py b/tests/test_api.py
deleted file mode 100644
index c7b38e35..00000000
--- a/tests/test_api.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env python
-import logging
-import sys
-import unittest
-
-import numpy as np
-from api_client import APIconnection
-
-logging.basicConfig(stream=sys.stderr, level=logging.INFO)
-
-
-class TestCapture(unittest.TestCase):
- def test_capture_config(self):
- connection = APIconnection(host="localhost", port=5000, api_ver="v1")
- config = connection.get_config()
-
- expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"]
-
- for key in expected_keys:
- self.assertTrue(key in config)
-
- def test_capture_videoport(self):
- connection = APIconnection(host="localhost", port=5000, api_ver="v1")
- resolution = connection.get_config()["stream_resolution"]
-
- for resize in [None, (640, 480)]:
-
- if resize:
- resolution = resize
-
- capture_array = connection.capture(
- use_video_port=True,
- keep_on_disk=False,
- delete_after_use=True,
- resize=resize,
- )
-
- self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
-
- def test_capture_full(self):
- connection = APIconnection(host="localhost", port=5000, api_ver="v1")
- resolution = connection.get_config()["image_resolution"]
-
- for resize in [None, (640, 480)]:
-
- if resize:
- resolution = resize
-
- capture_array = connection.capture(
- use_video_port=False,
- keep_on_disk=False,
- delete_after_use=True,
- resize=resize,
- )
-
- self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
-
-
-class TestStage(unittest.TestCase):
- def test_stage_config(self):
- connection = APIconnection(host="localhost", port=5000, api_ver="v1")
- config = connection.get_config()
-
- expected_keys = ["backlash"]
-
- for key in expected_keys:
- self.assertTrue(key in config)
-
- def test_stage_state(self):
- connection = APIconnection(host="localhost", port=5000, api_ver="v1")
- state = connection.get_state()
-
- self.assertTrue("stage" in state)
-
- expected_keys = ["position"]
-
- for key in expected_keys:
- self.assertTrue(key in state["stage"])
-
- def test_stage_movement(self):
- connection = APIconnection(host="localhost", port=5000, api_ver="v1")
-
- move_distance = 500
- for axis in range(3):
- for direction in [1, -1]:
- pos_i_dict = connection.get_state()["stage"]["position"]
- pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]]
-
- move = [0, 0, 0]
- move[axis] = move_distance * direction
-
- connection.move_by(*move)
-
- pos_f_dict = connection.get_state()["stage"]["position"]
- pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]]
-
- diff = np.subtract(pos_f, pos_i)
- logging.debug("{} > {}".format(pos_i, pos_f))
-
- self.assertTrue(np.array_equal(diff, move))
-
-
-if __name__ == "__main__":
-
- suites = [
- unittest.TestLoader().loadTestsFromTestCase(TestCapture),
- unittest.TestLoader().loadTestsFromTestCase(TestStage),
- ]
-
- alltests = unittest.TestSuite(suites)
-
- result = unittest.TextTestRunner(verbosity=2).run(alltests)
diff --git a/tests/test_camera.py b/tests/test_camera.py
deleted file mode 100644
index 1d2723fb..00000000
--- a/tests/test_camera.py
+++ /dev/null
@@ -1,266 +0,0 @@
-#!/usr/bin/env python
-import io
-import logging
-import os
-import sys
-import time
-import unittest
-
-import numpy as np
-from PIL import Image
-
-from openflexure_microscope.camera.pi import CaptureObject, PiCameraStreamer
-
-logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
-
-
-class TestCaptureMethods(unittest.TestCase):
- def test_still_capture(self):
- """Tests capturing still images to a BytesIO stream."""
- global camera
-
- for use_video_port in [True, False]:
- for resize in [None, (640, 480)]:
-
- # Wait for camera
- camera.wait_for_camera()
-
- # Capture to a context (auto-deletes files when done)
- with camera.new_image() as output:
-
- camera.capture(output, use_video_port=use_video_port, resize=resize)
-
- # Ensure file deletion fails and returns False
- self.assertFalse(output.delete_file())
- # Ensure capture not stored to file
- self.assertFalse(os.path.isfile(output.file))
-
- # BEFORE DELETE: Ensure StreamObject 'stream' has
- # a valid BytesIO object and byte string
- self.assertTrue(isinstance(output.data, io.IOBase))
- self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
-
- # Save capture to file
- output.save_file()
- # Check file got saved
- self.assertTrue(os.path.isfile(output.file))
-
- # Delete file
- output.delete_file()
- # Check file got deleted
- self.assertFalse(os.path.isfile(output.file))
-
- # AFTER DELETE: Ensure StreamObject 'stream' has
- # a valid BytesIO object and byte string
- self.assertTrue(isinstance(output.data, io.IOBase))
- self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
-
- # Create a PIL image from stream
- image = Image.open(output.data)
-
- # Ensure a valid PIL image was created
- self.assertTrue(isinstance(image, Image.Image))
-
- # Calculate expected dimensions
- if resize:
- dims = resize
- else:
- if use_video_port:
- dims = camera.stream_resolution
- else:
- dims = camera.image_resolution
-
- # Ensure PIL image size matches expected size
- print(image.size, dims)
- self.assertTrue(image.size == dims)
-
- def test_still_store(self):
- """Tests capturing still images to a file on disk."""
- global camera
-
- for use_video_port in [True, False]:
- for resize in [None, (640, 480)]:
- # Wait for camera
- camera.wait_for_camera()
-
- # Capture
- output = camera.capture(
- camera.new_image().file,
- use_video_port=use_video_port,
- resize=resize,
- )
-
- # Check file got saved
- self.assertTrue(os.path.isfile(output.file))
- statinfo = os.stat(output.file)
- self.assertTrue(statinfo.st_size > 0)
-
- # Ensure StreamObject 'stream' has
- # a valid BytesIO object and byte string
- self.assertTrue(isinstance(output.data, io.IOBase))
- self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
-
- # Ensure file deletion completes and returns True
- self.assertTrue(output.delete_file())
-
- # Check file got deleted
- self.assertFalse(os.path.isfile(output.file))
-
-
-class TestUnencodedMethods(unittest.TestCase):
- def test_yuv_array(self):
- """Tests capturing unencoded YUV data to a Numpy array."""
- global camera
-
- for use_video_port in [True, False]:
- for resize in [None, (640, 480)]:
-
- print("{}, {}, {}".format(id, resize, use_video_port))
-
- # Wait for camera
- camera.wait_for_camera()
-
- # Capture RGB array
- yuv = camera.yuv(use_video_port=use_video_port, resize=resize)
-
- # Ensure capture output is a valid numpy array
- self.assertTrue(isinstance(yuv, np.ndarray))
-
- # Calculate expected dimensions
- if resize:
- dims = resize
- else:
- if use_video_port:
- dims = camera.stream_resolution
- else:
- dims = camera.numpy_resolution
-
- # Ensure array shape matches expected dimensions
- self.assertTrue(yuv.shape == (dims[1], dims[0], 3))
-
- def test_rgb_array(self):
- """Tests capturing unencoded YUV/RGB data to a Numpy array."""
- global camera
-
- for use_video_port in [True, False]:
- for resize in [None, (640, 480)]:
-
- print("{}, {}, {}".format(id, resize, use_video_port))
-
- # Wait for camera
- camera.wait_for_camera()
-
- # Capture RGB array
- rgb = camera.array(use_video_port=use_video_port, resize=resize)
-
- # Ensure capture output is a valid numpy array
- self.assertTrue(isinstance(rgb, np.ndarray))
-
- # Calculate expected dimensions
- if resize:
- dims = resize
- else:
- if use_video_port:
- dims = camera.stream_resolution
- else:
- dims = camera.numpy_resolution
-
- # Ensure array shape matches expected dimensions
- self.assertTrue(rgb.shape == (dims[1], dims[0], 3))
-
-
-class TestRecordMethods(unittest.TestCase):
- def test_video_record(self):
- """Tests recording videos to BytesIO stream, and to file on disk."""
- global camera
-
- # Wait for camera
- camera.wait_for_camera()
-
- with camera.new_video() as output:
-
- # Start recording
- camera.start_recording(output)
-
- # Record for 2 seconds
- time.sleep(2)
- # Stop recording
- camera.stop_recording()
-
- # Check stream
- self.assertTrue(isinstance(output.data, io.IOBase))
- self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
-
- # Check file
- statinfo = os.stat(output.file)
- self.assertTrue(statinfo.st_size > 0)
-
- # Log path
- temp_path = output.file
-
- # Check file got deleted on __exit__
- self.assertFalse(os.path.isfile(temp_path))
-
- time.sleep(0.25)
-
- def test_video_store(self):
- """Tests recording videos to file on disk, without context manager."""
- global camera
-
- # Wait for camera
- camera.wait_for_camera()
-
- # Start recording
- output = camera.start_recording(camera.new_video())
-
- # Record for 2 seconds
- time.sleep(2)
- # Stop recording
- camera.stop_recording()
-
- # Check file
- statinfo = os.stat(output.file)
- self.assertTrue(statinfo.st_size > 0)
-
- # Ensure file deletion completes and returns True
- self.assertTrue(output.delete_file())
-
- # Check file got deleted
- self.assertFalse(os.path.isfile(output.file))
-
-
-class TestThreadStarting(unittest.TestCase):
- def test_restarting_stream(self):
- """Tests that a capture call restarts the camera worker thread."""
- global camera
-
- # Wait for camera
- camera.wait_for_camera()
-
- # Force-stop the camera worker thread (should return True)
- self.assertTrue(camera.stop_worker())
-
- # Check Pi camera has been disconnected
- self.assertTrue(camera.camera)
-
- # Restart worker thread
- self.assertTrue(camera.start_worker())
-
- self.assertTrue(camera.camera)
- self.assertTrue(camera.thread)
- self.assertIsInstance(camera.stream, io.IOBase)
-
-
-if __name__ == "__main__":
- with PiCameraStreamer() as camera:
-
- suites = [
- unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),
- unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods),
- unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting),
- unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods),
- ]
-
- alltests = unittest.TestSuite(suites)
-
- result = unittest.TextTestRunner(verbosity=2).run(alltests)
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
deleted file mode 100644
index 431e70a8..00000000
--- a/tests/test_plugins.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env python
-import atexit
-import logging
-import sys
-import unittest
-
-from openflexure_stage import OpenFlexureStage
-
-from openflexure_microscope import Microscope, config
-from openflexure_microscope.camera.pi import PiCameraStreamer
-
-logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
-
-
-class TestPluginMethods(unittest.TestCase):
- def test_plugin_load(self):
- plugin_arr = microscope.plugins.plugins
-
- plugin_names = [plugin[0] for plugin in plugin_arr]
-
- self.assertTrue("testing" in plugin_names)
-
- def test_camera_access(self):
- identify = microscope.plugins.testing.identify()
- self.assertTrue(identify[0] is microscope.camera)
-
- def test_stage_access(self):
- identify = microscope.plugins.testing.identify()
- self.assertTrue(identify[1] is microscope.stage)
-
-
-if __name__ == "__main__":
-
- with Microscope() as microscope:
-
- microscope.attach(PiCameraStreamer(), OpenFlexureStage())
-
- microscope.plugins.attach("openflexure_microscope.plugins.testing:Plugin")
-
- suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)]
-
- alltests = unittest.TestSuite(suites)
-
- result = unittest.TextTestRunner(verbosity=2).run(alltests)
diff --git a/tests/test_stage.py b/tests/test_stage.py
deleted file mode 100644
index 90ee10d4..00000000
--- a/tests/test_stage.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env python
-import logging
-import sys
-import unittest
-
-import numpy as np
-from openflexure_stage import OpenFlexureStage
-
-logging.basicConfig(stream=sys.stderr, level=logging.INFO)
-
-
-class TestMicroscope(unittest.TestCase):
- def test_movement(self):
- move_distance = 500
- for axis in range(3):
- for direction in [1, -1]:
- pos_i = stage.position
- logging.debug(pos_i)
-
- logging.info(
- "Moving axis {} by {}".format(axis, move_distance * direction)
- )
- move = [0, 0, 0]
- move[axis] = move_distance * direction
-
- stage.move_rel(move)
-
- pos_f = stage.position
- diff = np.subtract(pos_f, pos_i)
- logging.debug("{} > {}".format(pos_i, pos_f))
-
- self.assertTrue(np.array_equal(diff, move))
-
-
-if __name__ == "__main__":
- with OpenFlexureStage("/dev/ttyUSB0") as stage:
-
- suites = [unittest.TestLoader().loadTestsFromTestCase(TestMicroscope)]
-
- alltests = unittest.TestSuite(suites)
-
- result = unittest.TextTestRunner(verbosity=2).run(alltests)