Merge branch 'master' into 'rwb27-plugin-comments'

# Conflicts:
#   docs/source/extensions/structure.rst
This commit is contained in:
Joel Collins 2020-03-24 11:17:11 +00:00
commit 7c59245753
65 changed files with 1829 additions and 844 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
openflexure_microscope/api/static/** filter=lfs diff=lfs merge=lfs -text

View file

@ -226,7 +226,7 @@ intersphinx_mapping = {
"openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None),
"picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None),
"marshmallow": ("https://marshmallow.readthedocs.io/en/stable/", None),
"webargs": ("https://webargs.readthedocs.io/en/latest/", None)
"webargs": ("https://webargs.readthedocs.io/en/latest/", None),
}
# -- Options for todo extension ----------------------------------------------

View file

@ -0,0 +1,41 @@
from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Create some instance variable
self.state_variable = "An example of a persistant instance variable"
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
def identify(self):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
def rename(self, new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
# Create your extension object
my_extension = MyExtension()

View file

@ -14,7 +14,7 @@ from labthings.server import fields
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string)

View file

@ -19,7 +19,7 @@ from labthings.server import fields
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string)

View file

@ -24,7 +24,7 @@ import io # Used in our capture action
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string)

View file

@ -0,0 +1,53 @@
Lifecycle Hooks
===============
Introduction
------------
In some cases it is useful to have functions triggered by events in an extensions lifecycle. Currently two such lifecycle events can be used, ``on_register``, and ``on_component``.
``on_register``
---------------
The ``on_register`` method can be used to have a function call as soon as the extension has been successfully registered to the microscope. For example:
.. code-block:: python
class MyExtension(BaseExtension):
def __init__(self):
# Track if the extension has been registered
self.registered = False
# Add lifecycle hooks
self.on_register(self.on_register_handler, args=(), kwargs={})
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
def on_register_handler(self, *args, **kwargs):
self.registered = True
print("Extension has been registered!")
``on_component``
----------------
The ``on_component`` method can be used to have a function call as soon as a particular LabThings component has been added. This can be used, for example, to get information about the microscope instance as soon as it is available. For example:
.. code-block:: python
class MyExtension(BaseExtension):
def __init__(self):
# Hold a reference to the microscope object as soon as it is available
self.microscope = None
# Add lifecycle hooks
self.on_component("com.myname.myextension", self.on_microscope_handler)
# Superclass init function
super().__init__("org.openflexure.microscope", version="0.0.0")
def on_microscope_handler(self, microscope_object):
print("Microscope object has been found!")
self.microscope = microscope_object

View file

@ -107,7 +107,7 @@ We start by creating a schema to describe how to serialise a :py:class:`openflex
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string)

View file

@ -24,3 +24,13 @@ Once this extension is loaded, any other extensions will have access to your met
# Call a function from your extension
if my_found_extension:
my_found_extension.identify()
Subclassing ``BaseExtension``
-------------------------------
The syntax used above allows novice programmers to easily start building extensions, without having to deal with subclassing. However, for more complex extensions which require persistent state, subclassing :py:class:`labthings.server.extensions.BaseExtension` is recommended.
The same simple extension as seen above can be written using subclassing:
.. literalinclude:: ./example_extension/01b_basic_structure_subclass.py

View file

@ -11,4 +11,5 @@ Developing API Extensions
./extensions/properties.rst
./extensions/actions.rst
./extensions/tasks_locks.rst
./extensions/ev_gui.rst
./extensions/ev_gui.rst
./extensions/lifecycle_hooks.rst

View file

@ -1,6 +0,0 @@
__all__ = ["Microscope", "config", "utilities"]
__version__ = "0.1.0"
from .microscope import Microscope
from . import config
from . import utilities

View file

@ -2,7 +2,7 @@
import time
import atexit
import logging
import logging, logging.handlers
import os
import pkg_resources
@ -16,10 +16,10 @@ from openflexure_microscope.api.utilities import list_routes, init_default_exten
from openflexure_microscope.config import JSONEncoder
from openflexure_microscope.paths import (
OPENFLEXURE_ETC_PATH,
OPENFLEXURE_VAR_PATH,
OPENFLEXURE_EXTENSIONS_PATH,
settings_file_path,
logs_file_path,
)
from labthings.server.quick import create_app
@ -30,37 +30,29 @@ from openflexure_microscope.api.microscope import default_microscope as api_micr
from openflexure_microscope.api.v2 import views
# Handle logging
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log")
DEFAULT_LOGFILE = logs_file_path("openflexure_microscope.log")
logger = logging.getLogger()
if (__name__ == "__main__") or (not is_gunicorn):
# If imported, but not by gunicorn
print("Letting sys handle logs")
logger.setLevel(logging.DEBUG)
else:
# Direct standard Python logging to file and console
error_formatter = logging.Formatter(
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
)
rotating_logfile = logging.handlers.RotatingFileHandler(
DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7
)
error_formatter = logging.Formatter(
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
)
error_handlers = [rotating_logfile, logging.StreamHandler()]
rotating_logfile = logging.handlers.RotatingFileHandler(
DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7
)
for handler in error_handlers:
handler.setFormatter(error_formatter)
logger.addHandler(handler)
error_handlers = [rotating_logfile, logging.StreamHandler()]
logger.setLevel(logging.getLogger("gunicorn.error").level)
for handler in error_handlers:
handler.setFormatter(error_formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Log server paths being used
logging.info(f"Running with data path {OPENFLEXURE_VAR_PATH}")
logging.info(f"Running with server path {OPENFLEXURE_ETC_PATH}")
# Create flask app
app, labthing = create_app(
@ -69,6 +61,7 @@ app, labthing = create_app(
title=f"OpenFlexure Microscope {api_microscope.name}",
description="Test LabThing-based API for OpenFlexure Microscope",
version=pkg_resources.get_distribution("openflexure_microscope").version,
flask_kwargs={"static_url_path": ""},
)
# Enable CORS for some routes outside of LabThings
@ -93,15 +86,21 @@ labthing.add_root_link(views.CaptureList, "captures")
labthing.add_view(views.CaptureView, f"/captures/<id>")
labthing.add_view(views.CaptureDownload, f"/captures/<id>/download/<filename>")
labthing.add_view(views.CaptureTags, f"/captures/<id>/tags")
labthing.add_view(views.CaptureMetadata, f"/captures/<id>/metadata")
labthing.add_view(views.CaptureAnnotations, f"/captures/<id>/annotations")
# Attach settings and state resources
labthing.add_view(views.SettingsProperty, f"/instrument/settings")
labthing.add_root_link(views.SettingsProperty, "instrumentSettings")
labthing.add_view(views.NestedSettingsProperty, "/instrument/settings/<path:route>")
labthing.add_view(views.StateProperty, "/instrument/state")
labthing.add_view(views.NestedStateProperty, "/instrument/state/<path:route>")
labthing.add_root_link(views.StateProperty, "instrumentState")
labthing.add_view(views.ConfigurationProperty, "/instrument/configuration")
labthing.add_view(
views.NestedConfigurationProperty, "/instrument/configuration/<path:route>"
)
labthing.add_root_link(views.ConfigurationProperty, "instrumentConfiguration")
# Attach settings and status resources
labthing.add_view(views.SettingsProperty, f"/settings")
labthing.add_root_link(views.SettingsProperty, "settings")
labthing.add_view(views.NestedSettingsProperty, "/settings/<path:route>")
labthing.add_view(views.StatusProperty, "/status")
labthing.add_view(views.NestedStatusProperty, "/status/<path:route>")
labthing.add_root_link(views.StatusProperty, "status")
# Attach streams resources
labthing.add_view(views.MjpegStream, f"/streams/mjpeg")
@ -109,12 +108,18 @@ labthing.add_view(views.SnapshotStream, f"/streams/snapshot")
# Attach microscope action resources
labthing.add_view(views.actions.ActionsView, "/actions")
labthing.add_root_link(views.actions.ActionsView, "actions")
for name, action in views.enabled_root_actions().items():
view_class = action["view_class"]
rule = action["rule"]
labthing.add_view(view_class, f"/actions{rule}")
@app.route("/")
def openflexure_ev():
return app.send_static_file("index.html")
@app.route("/routes")
@cross_origin()
def routes():

View file

@ -4,6 +4,7 @@ import traceback
from .autofocus import autofocus_extension_v2
from .scan import scan_extension_v2
from .zip_builder import zip_extension_v2
from .autostorage import autostorage_extension_v2
# "Gracefully" handle cases where picamera cannot be imported (eg test server)
try:
@ -11,4 +12,4 @@ try:
except Exception as e:
logging.error(
f"Exception loading builtin extension picamera_autocalibrate: \n{traceback.format_exc()}"
)
)

View file

@ -1,10 +1,7 @@
from labthings.server.find import find_component
from labthings.server.extensions import BaseExtension
from labthings.server.view import View
from labthings.server.decorators import (
ThingAction,
ThingProperty,
)
from labthings.server.decorators import ThingAction, ThingProperty, marshal_task
from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort
from openflexure_microscope.utilities import set_properties
@ -198,7 +195,7 @@ def move_and_find_focus(microscope, dz):
def fast_autofocus(microscope, dz=2000, backlash=None):
"""Perform a down-up-down-up autofocus"""
with monitor_sharpness(microscope) as m:
with monitor_sharpness(microscope) as m, microscope.camera.lock:
i, z = m.focus_rel(-dz / 2)
i, z = m.focus_rel(dz)
fz = m.sharpest_z_on_move(i)
@ -211,7 +208,7 @@ def fast_autofocus(microscope, dz=2000, backlash=None):
def fast_up_down_up_autofocus(
microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150
microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=25
):
"""Autofocus by measuring on the way down, and moving back up with feedback.
@ -242,7 +239,7 @@ def fast_up_down_up_autofocus(
from the starting position. Mostly useful if you're able to combine
the initial move with something else, e.g. moving to the next scan point.
mini_backlash: (optional, default 50) is a small extra move made in step
mini_backlash: (optional, default 25) is a small extra move made in step
3 to help counteract backlash. It should be small enough that you
would always expect there to be greater backlash than this. Too small
might slightly hurt accuracy, but is unlikely to be a big issue. Too big
@ -308,6 +305,7 @@ class AutofocusAPI(View):
Run a standard autofocus
"""
@marshal_task
def post(self):
payload = JsonResponse(request)
microscope = find_component("org.openflexure.microscope")
@ -323,7 +321,7 @@ class AutofocusAPI(View):
task = taskify(autofocus)(microscope, dz)
# return a handle on the autofocus task
return jsonify(task.state), 201
return task
else:
abort(503, "No stage connected. Unable to autofocus.")
@ -335,6 +333,7 @@ class FastAutofocusAPI(View):
Run a fast autofocus
"""
@marshal_task
def post(self):
payload = JsonResponse(request)
microscope = find_component("org.openflexure.microscope")
@ -344,24 +343,27 @@ class FastAutofocusAPI(View):
# Figure out the parameters to use
dz = payload.param("dz", default=2000, convert=int)
backlash = payload.param("backlash", default=0, convert=int)
backlash = payload.param("backlash", default=25, convert=int)
if backlash < 0:
backlash = 0
if microscope.has_real_stage():
logging.info("Running autofocus...")
task = taskify(fast_autofocus)(microscope, dz, backlash=backlash)
task = taskify(fast_up_down_up_autofocus)(microscope, dz=dz, mini_backlash=backlash)
# return a handle on the autofocus task
return jsonify(task.state), 201
return task
else:
abort(503, "No stage connected. Unable to autofocus.")
autofocus_extension_v2 = BaseExtension("org.openflexure.autofocus", version="2.0.0-beta.1")
autofocus_extension_v2 = BaseExtension(
"org.openflexure.autofocus", version="2.0.0"
)
autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus")
autofocus_extension_v2.add_method(fast_up_down_up_autofocus, "fast_up_down_up_autofocus")
autofocus_extension_v2.add_method(autofocus, "autofocus")
autofocus_extension_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness")

View file

@ -0,0 +1,271 @@
from labthings.server.extensions import BaseExtension
from labthings.server.view import View
from labthings.server.decorators import ThingProperty, PropertySchema, use_args
from labthings.server import fields
from labthings.server.find import find_component
from openflexure_microscope.paths import settings_file_path, check_rw
from openflexure_microscope.config import OpenflexureSettingsFile
from openflexure_microscope.camera.base import BASE_CAPTURE_PATH
from openflexure_microscope.camera.capture import build_captures_from_exif
from openflexure_microscope.api.utilities.gui import build_gui
from flask import abort
import logging
import os
import psutil
from sys import platform
AS_SETTINGS_PATH = settings_file_path("autostorage_settings.json")
def get_partitions():
return [disk.mountpoint for disk in psutil.disk_partitions() if "rw" in disk.opts]
def get_permissive_partitions():
return [partition for partition in get_partitions() if check_rw(partition)]
def get_permissive_locations():
return [
(partition, os.path.join(partition, "openflexure", "data", "micrographs"))
for partition in get_permissive_partitions()
]
def get_current_location(camera):
return camera.paths.get("default")
def set_current_location(camera, location: str):
if not os.path.isdir(location):
os.makedirs(location)
logging.debug("Updating location...")
camera.paths.update({"default": location})
logging.debug("Rebuilding captures...")
camera.rebuild_captures()
logging.debug("Capture location changed successfully.")
def get_default_location():
return BASE_CAPTURE_PATH
def get_all_locations():
locations = {}
# If default is not already listed (e.g. if it's currently set)
if get_default_location() not in locations.values():
locations["Default"] = get_default_location()
for ppartition, plocation in get_permissive_locations():
pdrive = os.path.splitdrive(plocation)[0]
if not (
pdrive # If path actually has a drive (basically just Windows?)
and any( # And shares a common drive with an existing location
[
pdrive == os.path.splitdrive(location)[0]
for location in locations.values()
]
)
):
locations[ppartition] = plocation
# Strip out Nones
return {k: v for k, v in locations.items() if v}
class CaptureStorageLocation:
def __init__(self, mountpoint):
pass
class AutostorageExtension(BaseExtension):
def __init__(self):
BaseExtension.__init__(
self,
"org.openflexure.autostorage",
version="2.0.0",
description="Handle switching capture storage devices",
)
# We'll store a reference to a camera object, who's capture paths will be modified
self.camera = None
self.initial_location = get_default_location()
# Register the on_microscope function to run when the microscope is attached
self.on_component("org.openflexure.microscope", self.on_microscope)
def on_microscope(self, microscope_obj):
"""Function to automatically call when the parent LabThing has a microscope attached."""
logging.debug(f"Autostorage extension found microscope {microscope_obj}")
if hasattr(microscope_obj, "camera"):
logging.debug(f"Autostorage extension bound to camera {self.camera}")
# Store a reference to the camera
self.camera = microscope_obj.camera
# Store the initial storage location
self.initial_location = get_current_location(self.camera)
# If preferred path does not exist, or cannot be written to
self.check_location(self.initial_location)
logging.debug(self.get_locations())
def check_location(self, location=None):
if not location:
location = get_current_location(self.camera)
# If preferred path does not exist, or cannot be written to
if not (os.path.isdir(location) and check_rw(location)):
logging.error(
f"Preferred capture path {location} is missing or cannot be written to. Restoring defaults."
)
# Reset the storage location to default
set_current_location(self.camera, get_default_location())
def get_locations(self):
if self.camera:
locations = get_all_locations()
current_location = get_current_location(self.camera)
if current_location not in locations.values():
locations.update({"Custom": current_location})
# Add location from the cameras settings file
return locations
else:
return {}
def get_preferred_key(self):
current = get_current_location(self.camera)
locations = self.get_locations()
matches = [k for k, v in locations.items() if v == current]
if len(matches) > 1:
logging.warning(
"Multiple path matches found. Weird, but carrying on using zeroth."
)
return matches[0]
def set_preferred_key(self, new_path_key: str):
if not new_path_key in self.get_locations().keys():
raise KeyError(f"No location named {new_path_key}")
location = self.get_locations().get(new_path_key)
set_current_location(self.camera, location)
def key_to_title(self, path_key: str):
if not path_key in self.get_locations().keys():
raise KeyError(f"No location named {path_key}")
return f"{path_key} ({self.get_locations().get(path_key)})"
def title_to_key(self, path_title: str):
matches = []
for loc_key in self.get_locations().keys():
if path_title.startswith(loc_key):
matches.append(loc_key)
if len(matches) > 1:
logging.warning(
"Multiple path matches found. Weird, but carrying on using zeroth."
)
return matches[0]
def get_titles(self):
return [self.key_to_title(key) for key in self.get_locations().keys()]
def get_preferred_title(self):
return self.key_to_title(self.get_preferred_key())
autostorage_extension_v2 = AutostorageExtension()
@ThingProperty
class GetLocationsView(View):
def get(self):
global autostorage_extension_v2
autostorage_extension_v2.check_location()
return autostorage_extension_v2.get_locations()
@ThingProperty
@PropertySchema(fields.String(required=True, example="Default"))
class PreferredLocationView(View):
def get(self):
global autostorage_extension_v2
autostorage_extension_v2.check_location()
return autostorage_extension_v2.get_preferred_key()
def post(self, new_path_key):
global autostorage_extension_v2
microscope = find_component("org.openflexure.microscope")
if not microscope:
abort(503, "No microscope connected. Unable to autofocus.")
autostorage_extension_v2.check_location()
autostorage_extension_v2.set_preferred_key(new_path_key)
microscope.save_settings()
class PreferredLocationGUIView(View):
@use_args({"new_path_title": fields.String(required=True)})
def post(self, args):
global autostorage_extension_v2
new_path_title = args.get("new_path_title")
logging.debug(new_path_title)
new_path_key = autostorage_extension_v2.title_to_key(new_path_title)
logging.debug(f"{new_path_key}")
autostorage_extension_v2.check_location()
autostorage_extension_v2.set_preferred_key(new_path_key)
return new_path_title
def dynamic_form():
global autostorage_extension_v2
autostorage_extension_v2.check_location()
return {
"icon": "sd_storage",
"title": "Storage",
"forms": [
{
"name": "Autostorage",
"isCollapsible": False,
"isTask": False,
"route": "/location-from-title",
"emitOnResponse": "globalUpdateCaptures",
"submitLabel": "Set path",
"schema": [
{
"fieldType": "selectList",
"name": "new_path_title",
"label": "Capture storage path",
"options": autostorage_extension_v2.get_titles(),
"value": autostorage_extension_v2.get_preferred_title(),
}
],
}
],
}
autostorage_extension_v2.add_view(GetLocationsView, "list-locations")
autostorage_extension_v2.add_view(PreferredLocationView, "location")
autostorage_extension_v2.add_view(PreferredLocationGUIView, "location-from-title")
autostorage_extension_v2.add_meta(
"gui", build_gui(dynamic_form, autostorage_extension_v2)
)

View file

@ -1,10 +1,7 @@
from labthings.server.view import View
from labthings.server.find import find_component
from labthings.server.extensions import BaseExtension
from labthings.server.decorators import (
marshal_task,
ThingAction,
)
from labthings.server.decorators import marshal_task, ThingAction
from labthings.core.tasks import taskify
@ -24,8 +21,8 @@ def recalibrate(microscope):
"""
scamera = microscope.camera
with scamera.lock:
assert not scamera.status["record_active"], "Can't recalibrate while recording!"
streaming = scamera.status["stream_active"]
assert not scamera.record_active, "Can't recalibrate while recording!"
streaming = scamera.stream_active
if streaming:
logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480))

View file

@ -1,20 +1,14 @@
import itertools
import logging
import uuid
import datetime
from typing import Tuple
from functools import reduce
from openflexure_microscope.camera.base import generate_basename
from labthings.server.find import (
find_component,
find_extension,
)
from labthings.server.find import find_component, find_extension
from labthings.server.extensions import BaseExtension
from labthings.server.decorators import (
marshal_task,
use_args,
ThingAction,
)
from labthings.server.decorators import marshal_task, use_args, ThingAction
from labthings.server import fields
from openflexure_microscope.devel import taskify, abort, update_task_progress
@ -78,12 +72,12 @@ def progress():
def capture(
microscope,
basename,
scan_id,
temporary: bool = False,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = False,
metadata: dict = {},
annotations: dict = {},
tags: list = [],
):
@ -101,16 +95,14 @@ def capture(
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
)
# Affix metadata
if "scan" not in tags:
tags.append("scan")
# Inject system metadata
output.put_metadata(microscope.metadata, system=True)
output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata
output.put_metadata(metadata)
# Insert custom metadata
output.put_annotations(annotations)
# Insert custom tags
output.put_tags(tags)
@ -122,7 +114,7 @@ def tile(
microscope,
basename: str = None,
temporary: bool = False,
step_size: int = [2000, 1500, 100],
stride_size: int = [2000, 1500, 100],
grid: list = [3, 3, 5],
style="raster",
autofocus_dz: int = 50,
@ -131,13 +123,13 @@ def tile(
bayer: bool = False,
fast_autofocus=False,
metadata: dict = {},
annotations: dict = {},
tags: list = [],
):
global _images_to_be_captured
global _images_captured_so_far
# Keep task progress
# TODO: Make this line not nasty
_images_to_be_captured = reduce((lambda x, y: x * y), grid)
_images_captured_so_far = 0
@ -145,28 +137,22 @@ def tile(
if not basename:
basename = generate_basename()
# Generate a stack ID
scan_id = uuid.uuid4()
# Store initial position
initial_position = microscope.stage.position
# Add scan metadata
if "time" not in metadata:
metadata["time"] = generate_basename()
metadata.update(
{
"scan_id": scan_id,
"basename": basename,
"scan_parameters": {
"step_size": step_size,
"grid": grid,
"style": style,
"autofocus_dz": autofocus_dz,
},
# Add dataset metadata
dataset_d = {
"dataset": {
"id": uuid.uuid4(),
"type": "xyzScan",
"name": basename,
"acquisitionDate": datetime.datetime.now().isoformat(),
"strideSize": stride_size,
"grid": grid,
"style": style,
"autofocusDz": autofocus_dz,
}
)
}
# Check if autofocus is enabled
autofocus_extension = find_extension("org.openflexure.autofocus")
@ -180,25 +166,19 @@ def tile(
else:
autofocus_enabled = False
z_stack_dz = (
grid[2] * step_size[2] if grid[2] > 1 else 0
) # shorthand for Z stack range
# Construct an x-y grid (worry about z later)
x_y_grid = construct_grid(initial_position, step_size[:2], grid[:2], style=style)
x_y_grid = construct_grid(initial_position, stride_size[:2], grid[:2], style=style)
# Keep the initial Z position the same as our current position
next_z = initial_position[2]
if fast_autofocus: # If fast autofocus is enabled, make
next_z += autofocus_dz / 2 # sure we start from the top of the range
initial_z = next_z # Save this value for use in raster scans
initial_z = initial_position[2]
next_z = initial_z # Save this value for use in raster scans
# Now step through each point in the x-y coordinate array
for line in x_y_grid:
# If rastering, rather than snake (or eventually spiral)
# Return focus to initial position
if style == "raster":
next_z = initial_z
next_z = initial_z # Reset z position at start of each new row
logging.debug("Returning to initial z position")
microscope.stage.move_abs(
[line[0][0], line[0][1], next_z]
@ -211,31 +191,29 @@ def tile(
# Refocus
if autofocus_enabled:
if fast_autofocus:
autofocus_extension.fast_autofocus(
dz=autofocus_dz,
target_z=-z_stack_dz / 2.0, # Finish below the focus
initial_move_up=False, # We're already at the top of the scan
# Run fast autofocus. Client should provide dz ~ 2000
autofocus_extension.fast_up_down_up_autofocus(
microscope, dz=autofocus_dz
)
# TODO: save the focus data for future reference? Use it for diagnostics?
else:
logging.debug("Running autofocus")
# Run slow autofocus. Client should provide dz ~ 50
autofocus_extension.autofocus(
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
)
logging.debug("Finished autofocus")
time.sleep(1) # TODO: Remove
time.sleep(1)
# If we're not doing a z-stack, just capture
if grid[2] <= 1:
capture(
microscope,
basename,
scan_id,
temporary=temporary,
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=metadata,
metadata=dataset_d,
annotations=annotations,
tags=tags,
)
# Update task progress
@ -247,27 +225,17 @@ def tile(
microscope=microscope,
basename=basename,
temporary=temporary,
scan_id=scan_id,
step_size=step_size[2],
step_size=stride_size[2],
steps=grid[2],
center=not fast_autofocus, # fast_autofocus does this for us!
return_to_start=not fast_autofocus,
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=metadata,
metadata=dataset_d,
annotations=annotations,
tags=tags,
)
# Make sure we use our current best estimate of focus (i.e. the current position) next point
next_z = microscope.stage.position[2]
if fast_autofocus:
next_z += (
autofocus_dz / 2
) # Fast autofocus requires us to start at the top of the range
if grid[2] > 1:
next_z -= int(
grid[2] / 2.0 * step_size[2]
) # Z stacking means we're higher up to start with
logging.debug("Returning to {}".format(initial_position))
microscope.stage.move_abs(initial_position)
@ -277,52 +245,40 @@ def stack(
microscope,
basename: str = None,
temporary: bool = False,
scan_id: str = None,
step_size: int = 100,
steps: int = 5,
center: bool = True,
return_to_start: bool = True,
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = False,
metadata: dict = {},
annotations: dict = {},
tags: list = [],
):
global _images_captured_so_far
# Generate a basename if none given
if not basename:
basename = generate_basename()
# Generate a stack ID
if not scan_id:
scan_id = uuid.uuid4()
# Add scan metadata
if not "time" in metadata:
metadata["time"] = generate_basename()
# Store initial position
initial_position = microscope.stage.position
logging.debug(f"Starting z-stack from position {microscope.stage.position}")
with microscope.lock:
# Move to center scan
if center:
logging.debug("Moving to starting position")
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
logging.debug("Moving to z-stack starting position")
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
logging.debug(f"Starting scan from position {microscope.stage.position}")
for i in range(steps):
time.sleep(0.1)
logging.debug("Capturing...")
logging.debug(f"Capturing from position {microscope.stage.position}")
capture(
microscope,
basename,
scan_id,
temporary=temporary,
use_video_port=use_video_port,
resize=resize,
bayer=bayer,
metadata=metadata,
annotations=annotations,
tags=tags,
)
# Update task progress
@ -344,16 +300,18 @@ def stack(
class TileScanAPI(View):
@use_args(
{
"filename": fields.String(),
"filename": fields.String(missing=None, example=None),
"temporary": fields.Boolean(missing=False),
"step_size": fields.List(fields.Integer, missing=[2000, 1500, 100]),
"grid": fields.List(fields.Integer, missing=[3, 3, 5]),
"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),
"metadata": fields.Dict(missing={}),
"annotations": fields.Dict(missing={}, example={"Foo": "Bar"}),
"tags": fields.List(fields.String, missing=[]),
"resize": fields.Dict(missing=None), # TODO: Validate keys
}
@ -380,7 +338,7 @@ class TileScanAPI(View):
microscope,
basename=args.get("filename"),
temporary=args.get("temporary"),
step_size=args.get("step_size"),
stride_size=args.get("stride_size"),
grid=args.get("grid"),
style=args.get("style"),
autofocus_dz=args.get("autofocus_dz"),
@ -388,7 +346,7 @@ class TileScanAPI(View):
resize=resize,
bayer=args.get("bayer"),
fast_autofocus=args.get("fast_autofocus"),
metadata=args.get("metadata"),
annotations=args.get("annotations"),
tags=args.get("tags"),
)
@ -396,6 +354,6 @@ class TileScanAPI(View):
return task
scan_extension_v2 = BaseExtension("org.openflexure.scan", version="2.0.0-beta.1")
scan_extension_v2 = BaseExtension("org.openflexure.scan", version="2.0.0")
scan_extension_v2.add_view(TileScanAPI, "/tile")

View file

@ -6,7 +6,7 @@ from openflexure_microscope.devel import (
update_task_progress,
)
from flask import send_file, abort
from flask import send_file, abort, url_for
import uuid
import os
@ -16,13 +16,56 @@ import logging
from labthings.server.find import find_component
from labthings.server.view import View
from labthings.server.schema import Schema
from labthings.server import fields
from labthings.server.extensions import BaseExtension
from labthings.server.utilities import description_from_view
from labthings.server.decorators import (
ThingAction,
ThingProperty,
marshal_task,
marshal_with,
pre_dump,
)
class ZipObjectSchema(Schema):
id = fields.String()
data_size = fields.Number()
zip_size = fields.Number()
links = fields.Dict()
@pre_dump
def generate_links(self, data, **kwargs):
data.links = {
"download": {
"href": url_for(
ZipGetterAPIView.endpoint, session_id=data.id, _external=True
),
**description_from_view(ZipGetterAPIView),
}
}
return data
class ZipObjectDescription:
def __init__(self, id, file_pointer, data_size=None):
self.id = id
self.fp = file_pointer
self.data_size = data_size
self.zip_size = os.path.getsize(self.fp.name) * 1e-6
def close(self):
logging.debug(self.fp.name)
self.fp.close()
os.unlink(self.fp.name)
assert not os.path.exists(self.fp.name)
def __del__(self):
self.close()
class ZipManager:
"""
ZIP-builder manager
@ -38,8 +81,7 @@ class ZipManager:
# Get array of captures from IDs
capture_list = [
microscope.camera.image_from_id(capture_id)
for capture_id in capture_id_list
microscope.camera.images.get(capture_id) for capture_id in capture_id_list
]
# Remove Nones from list (missing/invalid captures)
capture_list = [capture for capture in capture_list if capture]
@ -76,20 +118,23 @@ class ZipManager:
# Update task progress
update_task_progress(int((index / n_files) * 100))
session_id = uuid.uuid4()
session_key = str(session_id)
# self.session_zips[session_id] = fp
self.session_zips[session_key] = {
"id": session_id,
"fp": fp,
"data_size": data_size_megabytes,
"zip_size": os.path.getsize(fp.name) * 1e-6,
}
session_id = str(uuid.uuid4())
session_description = ZipObjectDescription(
session_id, fp, data_size=data_size_megabytes
)
self.session_zips[session_id] = session_description
return self.session_zips[session_key]
return self.session_zips[session_id]
def zip_from_id(self, session_id):
return self.session_zips[session_id]["fp"]
def marshaled_build_zip_from_capture_ids(self, *args, **kwargs):
return ZipObjectSchema().dump(self.build_zip_from_capture_ids(*args, **kwargs))
def zip_fp_from_id(self, session_id):
return self.session_zips[session_id].fp
def __del__(self):
for zd in self.session_zips.values():
zd.close()
# Create a global ZIP manager
@ -98,56 +143,68 @@ default_zip_manager = ZipManager()
@ThingAction
class ZipBuilderAPIView(View):
@marshal_task
def post(self):
ids = list(JsonResponse(request).json)
microscope = find_component("org.openflexure.microscope")
task = taskify(default_zip_manager.build_zip_from_capture_ids)(microscope, ids)
task = taskify(default_zip_manager.marshaled_build_zip_from_capture_ids)(
microscope, ids
)
# Return a handle on the autofocus task
return jsonify(task.state), 201
return task
@ThingProperty
class ZipListAPIView(View):
@marshal_with(ZipObjectSchema(many=True))
def get(self):
return jsonify(default_zip_manager.session_zips)
return default_zip_manager.session_zips.values()
class ZipGetterAPIView(View):
"""
Download or delete a particular capture collection ZIP file
"""
def get(self, session_id):
"""
Download a particular capture collection ZIP file
"""
if not session_id in default_zip_manager.session_zips:
return abort(404) # 404 Not Found
logging.info(f"Session ID: {session_id}")
return send_file(
default_zip_manager.zip_from_id(session_id).name,
default_zip_manager.zip_fp_from_id(session_id).name,
mimetype="application/zip",
as_attachment=True,
attachment_filename=f"{session_id}.zip",
)
def delete(self, session_id):
"""
Close and delete a particular capture collection ZIP file
"""
if not session_id in default_zip_manager.session_zips:
return abort(404) # 404 Not Found
logging.info(f"Session ID: {session_id}")
fp = default_zip_manager.zip_from_id(session_id)
logging.debug(fp.name)
fp.close()
os.unlink(fp.name)
assert not os.path.exists(fp.name)
# Close the file
default_zip_manager.session_zips[session_id].close()
# Delete the file reference
del default_zip_manager.session_zips[session_id]
return jsonify({"return": session_id})
return {"return": session_id}
zip_extension_v2 = BaseExtension("org.openflexure.zipbuilder", version="2.0.0-beta.1")
zip_extension_v2 = BaseExtension(
"org.openflexure.zipbuilder",
version="2.0.0",
description="Build and download capture collections as ZIP files",
)
zip_extension_v2.add_view(ZipGetterAPIView, "/get/<string:session_id>")
zip_extension_v2.add_view(ZipListAPIView, "/get")

View file

@ -1,9 +1,6 @@
from labthings.server.view import View
from labthings.server.extensions import BaseExtension
from labthings.server.decorators import (
ThingAction,
ThingProperty,
)
from labthings.server.decorators import ThingAction, ThingProperty
from openflexure_microscope.api.utilities.gui import build_gui
@ -35,7 +32,7 @@ def dynamic_form():
"name": "val_int",
"label": "Number value",
"minValue": 0,
"value": val_int
"value": val_int,
},
{
"fieldType": "htmlBlock",
@ -66,7 +63,7 @@ static_form = {
"name": "val_int",
"label": "Number value",
"minValue": 0,
"default": 1
"default": 1,
},
{
"fieldType": "htmlBlock",

View file

@ -1,44 +1,12 @@
from openflexure_microscope import Microscope
from openflexure_microscope.camera.capture import build_captures_from_exif
from openflexure_microscope.microscope import Microscope
import logging
# Import device modules
# NB this will eventually be handled by the RC file, so you can choose what device
# class should be attached.
try:
from openflexure_microscope.camera.pi import PiCameraStreamer
except ImportError:
logging.warning("Unable to import PiCameraStreamer")
from openflexure_microscope.camera.mock import MockStreamer
from openflexure_microscope.stage.sanga import SangaStage
from openflexure_microscope.stage.mock import MockStage
default_microscope = Microscope()
# Initialise camera
logging.debug("Creating camera object...")
try:
api_camera = PiCameraStreamer()
except Exception as e:
logging.error(e)
logging.warning("No valid camera hardware found. Falling back to mock camera!")
api_camera = MockStreamer()
# Initialise stage
logging.debug("Creating stage object...")
api_stage = MockStage()
# Attach devices to microscope
logging.debug("Attaching devices to microscope...")
default_microscope.attach(api_camera, api_stage)
# Restore loaded capture array to camera object
logging.debug("Restoring captures...")
default_microscope.camera.images = build_captures_from_exif(
default_microscope.camera.paths["default"]
)
default_microscope.camera.rebuild_captures()
logging.debug("Microscope successfully attached!")

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8605f70737729f74f48d3183b30b4a2f50706f4f750cd208aeb8c136253f7d01
size 772

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e68506e72389393d35cc47e5184459fff9a23bedea8c9ee96491f66c88e71954
size 1375875

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee944cc13be0a52ce48a81a09c179dac70729bd7580a516ccc8ac10b4cfa3f98
size 1310

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:710327f8b3d83c83019ed2c4b27de8caf8e4e923c9148f93c1c0a2662f683f26
size 1659

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0405ef60ab42df1c2e8721bf1f500c8c7d9e1afae130307101b8f77925ffdac1
size 1150

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4a1baec300d09e03a8380b85918267ee80faae8e00c6c56b48e2e74b1d9b38d
size 57620

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a87d66c91b2e7dc5530aef76c03bd6a3d25ea5826110bf4803b561b811cc8726
size 44300

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7f4a3ab562048f28dd1fa691601bc43363a61d0f876d16d8316c52e4f32d696
size 128180

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c998b4a9c0acbb9fe5dd572c206a5a33fdd5ca2b58db87fc3b893beac85068d
size 143258

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:463bfdbd635ddff1d00df38b1149ffd3947b492d71cfb546549dffebd311b090
size 843

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a536bfa88fd1bf69284804285c5cf1da33cf05f164f175eb4085f7d057ff4f17
size 378133

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:996b19391845b7cf1810f1181d935aad524897763da4c36847c7715f762b63fa
size 1597950

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a77d7d2e5e4220883bc1491a627435cc80d0b2574d51972bcd6b12add7a4a94
size 116850

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1404c938e8f60b1694dd48357d486d919f5b1e32c9f226ca26fc0ee399019542
size 395811

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:187e61d8f6171eebf611502412556c707fab1d837f8763b2b60ce3915303f0ad
size 1734

View file

@ -3,6 +3,12 @@ import logging
from functools import wraps
def clean_rule(rule: str):
while rule[0] == "/":
rule = rule[1:]
return f"{rule}"
def build_gui_from_dict(gui_description, extension_object):
# Make a working copy of GUI description
api_gui = copy.deepcopy(gui_description)
@ -10,6 +16,10 @@ def build_gui_from_dict(gui_description, extension_object):
# Expand shorthand routes into full relative URLs
if "forms" in gui_description and isinstance(api_gui["forms"], list):
for form in api_gui["forms"]:
# Clean leading slashes from rule
if "route" in form:
form["route"] = clean_rule(form["route"])
# Match rule in extension object
if "route" in form and form["route"] in extension_object._rules.keys():
form["route"] = extension_object._rules[form["route"]]["rule"]
else:

View file

@ -1,4 +1,4 @@
from .actions import enabled_root_actions
from .captures import *
from .state import *
from .instrument import *
from .streams import *

View file

@ -1,7 +1,7 @@
"""
Top-level representation of enabled actions
"""
from flask import url_for
from . import camera, stage, system
from labthings.server.view import View
@ -74,9 +74,9 @@ class ActionsView(View):
d = {
"links": {
"self": {
"href": current_labthing().url_for(action["view_class"]),
"href": url_for(action["view_class"].endpoint, _external=True),
"mimetype": "application/json",
**description_from_view(action["view_class"])
**description_from_view(action["view_class"]),
}
},
"rule": action["rule"],

View file

@ -36,7 +36,7 @@ class CaptureAPI(View):
"bayer": fields.Boolean(
missing=False, description="Store raw bayer data in file"
),
"metadata": fields.Dict(missing={}, example={"Client": "SwaggerUI"}),
"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}
@ -75,11 +75,10 @@ class CaptureAPI(View):
)
# Inject system metadata
output.put_metadata(microscope.metadata, system=True)
output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata
output.put_metadata(args.get("metadata"))
output.put_annotations(args.get("annotations"))
# Insert custom tags
output.put_tags(args.get("tags"))
@ -132,10 +131,7 @@ class RAMCaptureAPI(View):
stream.seek(0)
return send_file(
io.BytesIO(stream.getbuffer()),
mimetype="image/jpeg"
)
return send_file(io.BytesIO(stream.getbuffer()), mimetype="image/jpeg")
@ThingAction
@ -167,8 +163,8 @@ class GPUPreviewStartAPI(View):
microscope.camera.start_preview(fullscreen=fullscreen, window=window)
# TODO: Make schema for microscope status
return jsonify(microscope.status)
# TODO: Make schema for microscope state
return jsonify(microscope.state)
@ThingAction
@ -179,5 +175,5 @@ class GPUPreviewStopAPI(View):
"""
microscope = find_component("org.openflexure.microscope")
microscope.camera.stop_preview()
# TODO: Make schema for microscope status
return jsonify(microscope.status)
# TODO: Make schema for microscope state
return jsonify(microscope.state)

View file

@ -1,12 +1,7 @@
from openflexure_microscope.api.utilities import JsonResponse
from labthings.server.view import View
from labthings.server.find import find_component
from labthings.server.decorators import (
use_args,
marshal_with,
doc,
ThingAction,
)
from labthings.server.decorators import use_args, marshal_with, doc, ThingAction
from labthings.server import fields
from openflexure_microscope.utilities import axes_to_array, filter_dict
@ -57,8 +52,8 @@ class MoveStageAPI(View):
else:
logging.warning("Unable to move. No stage found.")
# TODO: Make schema for microscope status
return jsonify(microscope.status["stage"]["position"])
# TODO: Make schema for microscope state
return jsonify(microscope.state["stage"]["position"])
@ThingAction
@ -71,5 +66,5 @@ class ZeroStageAPI(View):
microscope = find_component("org.openflexure.microscope")
microscope.stage.zero_position()
# TODO: Make schema for microscope status
return jsonify(microscope.status["stage"])
# TODO: Make schema for microscope state
return jsonify(microscope.state["stage"])

View file

@ -3,10 +3,7 @@ import subprocess
import os
from sys import platform
from labthings.server.decorators import (
ThingAction,
doc_response,
)
from labthings.server.decorators import ThingAction, doc_response
def is_raspberrypi(raise_on_errors=False):
@ -28,9 +25,14 @@ class ShutdownAPI(View):
"""
Attempt to shutdown the device
"""
subprocess.Popen(["sudo", "shutdown", "-h", "now"])
p = subprocess.Popen(
["sudo", "shutdown", "-h", "now"],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
return "{}", 201
out, err = p.communicate()
return {"out": out, "err": err}, 201
@ThingAction
@ -44,6 +46,11 @@ class RebootAPI(View):
"""
Attempt to reboot the device
"""
subprocess.Popen(["sudo", "shutdown", "-r", "now"])
p = subprocess.Popen(
["sudo", "shutdown", "-r", "now"],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
return "{}", 201
out, err = p.communicate()
return {"out": out, "err": err}, 201

View file

@ -6,9 +6,7 @@ from openflexure_microscope.api.utilities import get_bool, JsonResponse
from labthings.server.schema import Schema
from labthings.server import fields
from labthings.server.view import View
from labthings.server.utilities import (
description_from_view,
)
from labthings.server.utilities import description_from_view
from labthings.server.decorators import marshal_with, doc_response, Tag, ThingProperty
from labthings.server.find import find_component
@ -16,14 +14,38 @@ from labthings.server.find import find_component
from marshmallow import pre_dump
class InstrumentSchema(Schema):
id = fields.UUID()
configuration = fields.Dict()
settings = fields.Dict()
state = fields.Dict()
class CaptureMetadataImageSchema(Schema):
id = fields.UUID()
acquisitionDate = fields.String(format="date")
format = fields.String()
name = fields.String()
tags = fields.List(fields.String())
annotations = fields.Dict()
class CaptureMetadataSchema(Schema):
experimenter = fields.Dict() # TODO: Make schema
experimenterGroup = fields.Dict() # TODO: Make schema
dataset = fields.Dict() # TODO: Make schema
image = fields.Nested(CaptureMetadataImageSchema())
instrument = fields.Nested(InstrumentSchema())
class CaptureSchema(Schema):
id = fields.String()
file = fields.String(
data_key="path", description="Path of file on microscope device"
)
exists = fields.Bool(data_key="available")
filename = fields.String()
metadata = fields.Dict()
name = fields.String()
metadata = fields.Nested(CaptureMetadataSchema())
links = fields.Dict()
@ -41,16 +63,18 @@ class CaptureSchema(Schema):
"mimetype": "application/json",
**description_from_view(CaptureTags),
},
"metadata": {
"href": url_for(CaptureMetadata.endpoint, id=data.id, _external=True),
"annotations": {
"href": url_for(
CaptureAnnotations.endpoint, id=data.id, _external=True
),
"mimetype": "application/json",
**description_from_view(CaptureMetadata),
**description_from_view(CaptureAnnotations),
},
"download": {
"href": url_for(
CaptureDownload.endpoint,
id=data.id,
filename=data.filename,
filename=data.name,
_external=True,
),
"mimetype": "image/jpeg",
@ -64,6 +88,9 @@ capture_schema = CaptureSchema()
capture_list_schema = CaptureSchema(many=True)
from pprint import pprint
@ThingProperty
@Tag("captures")
class CaptureList(View):
@ -73,7 +100,7 @@ class CaptureList(View):
List all image captures
"""
microscope = find_component("org.openflexure.microscope")
image_list = microscope.camera.images
image_list = microscope.camera.images.values()
return image_list
@ -85,7 +112,7 @@ class CaptureView(View):
Description of a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
@ -97,12 +124,15 @@ class CaptureView(View):
Delete a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
# Delete the capture file
capture_obj.delete()
# Delete from capture list
del microscope.camera.images[id]
return "", 204
@ -115,7 +145,7 @@ class CaptureDownload(View):
Image data for a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
@ -150,7 +180,7 @@ class CaptureTags(View):
Get tags associated with a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
@ -162,7 +192,7 @@ class CaptureTags(View):
Add tags to a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
@ -177,12 +207,12 @@ class CaptureTags(View):
return jsonify(capture_obj.tags)
def delete(self, capture_id):
def delete(self, id):
"""
Delete tags from a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
@ -199,25 +229,25 @@ class CaptureTags(View):
@Tag("captures")
class CaptureMetadata(View):
class CaptureAnnotations(View):
def get(self, id):
"""
Get metadata associated with a single image capture
Get annotations associated with a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
return jsonify(capture_obj.metadata)
return jsonify(capture_obj.annotations)
def put(self, id):
"""
Update metadata for a single image capture
"""
microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id)
capture_obj = microscope.camera.images.get(id)
if not capture_obj:
return abort(404) # 404 Not Found
@ -228,7 +258,6 @@ class CaptureMetadata(View):
if type(data_dict) != dict:
return abort(400)
# TODO: Allow putting system metadata maybe?
capture_obj.put_metadata(data_dict)
capture_obj.put_annotations(data_dict)
return jsonify(capture_obj.metadata)
return jsonify(capture_obj.annotations)

View file

@ -1,10 +1,6 @@
from openflexure_microscope.api.utilities import JsonResponse
from labthings.core.utilities import (
get_by_path,
set_by_path,
create_from_path,
)
from labthings.core.utilities import get_by_path, set_by_path, create_from_path
from labthings.server.find import find_component
from labthings.server.view import View
@ -34,7 +30,7 @@ class SettingsProperty(View):
logging.debug("Updating settings from PUT request:")
logging.debug(payload.json)
microscope.apply_settings(payload.json)
microscope.update_settings(payload.json)
microscope.save_settings()
return self.get()
@ -69,24 +65,24 @@ class NestedSettingsProperty(View):
dictionary = create_from_path(keys)
set_by_path(dictionary, keys, payload.json)
microscope.apply_settings(dictionary)
microscope.update_settings(dictionary)
microscope.save_settings()
return self.get(route)
@ThingProperty
class StatusProperty(View):
class StateProperty(View):
def get(self):
"""
Show current read-only state of the microscope
"""
microscope = find_component("org.openflexure.microscope")
return jsonify(microscope.status)
return jsonify(microscope.state)
@Tag("properties")
class NestedStatusProperty(View):
class NestedStateProperty(View):
@doc_response(404, description="Status key cannot be found")
def get(self, route):
"""
@ -96,7 +92,35 @@ class NestedStatusProperty(View):
keys = route.split("/")
try:
value = get_by_path(microscope.status, keys)
value = get_by_path(microscope.state, keys)
except KeyError:
return abort(404)
return jsonify(value)
@ThingProperty
class ConfigurationProperty(View):
def get(self):
"""
Show current read-only state of the microscope
"""
microscope = find_component("org.openflexure.microscope")
return jsonify(microscope.configuration)
@Tag("properties")
class NestedConfigurationProperty(View):
@doc_response(404, description="Configuration key cannot be found")
def get(self, route):
"""
Show a nested section of the current microscope state
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
try:
value = get_by_path(microscope.configuration, keys)
except KeyError:
return abort(404)

View file

@ -1,10 +1,6 @@
from openflexure_microscope.api.utilities import gen, JsonResponse
from labthings.core.utilities import (
get_by_path,
set_by_path,
create_from_path,
)
from labthings.core.utilities import get_by_path, set_by_path, create_from_path
from labthings.server.find import find_component
from labthings.server.view import View

View file

@ -7,8 +7,9 @@ import datetime
import logging
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from .capture import CaptureObject
from .capture import CaptureObject, build_captures_from_exif
from openflexure_microscope.utilities import entry_by_uuid
from labthings.core.lock import StrictLock
@ -91,20 +92,6 @@ class CameraEvent(object):
class BaseCamera(metaclass=ABCMeta):
"""
Base implementation of StreamingCamera.
Attributes:
thread: Background thread reading frames from camera
camera: Camera object
lock (:py:class:`labthings.lock.StrictLock`): Strict lock controlling thread
access to stage hardware
frame (bytes): Current frame is stored here by background thread
last_access (time): Time of last client access to the camera
stream_timeout (int): Number of inactive seconds before timing out the stream
stream_timeout_enabled (bool): Enable or disable timing out the stream
status (dict): Dictionary for capture state
paths (dict): Dictionary of capture paths
images (list): List of image capture objects
videos (list): List of video capture objects
"""
def __init__(self):
@ -121,16 +108,33 @@ class BaseCamera(metaclass=ABCMeta):
self.stream_timeout = 20
self.stream_timeout_enabled = False
self.status = {"board": None}
self.stream_active = False
self.record_active = False
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
# Capture data
self.images = []
self.videos = []
self.images = OrderedDict()
self.videos = OrderedDict()
@property
@abstractmethod
def configuration(self):
"""The current camera configuration."""
pass
@property
@abstractmethod
def state(self):
"""The current read-only camera state."""
pass
@property
def settings(self):
return self.read_settings()
@abstractmethod
def apply_settings(self, config: dict):
def update_settings(self, config: dict):
"""Update settings from a config dictionary"""
with self.lock:
# Apply valid config params to camera object
@ -143,10 +147,6 @@ class BaseCamera(metaclass=ABCMeta):
"""Return the current settings as a dictionary"""
return {"paths": self.paths}
def save_settings(self):
"""(Optional) Save any settings to disk that need to be stored"""
return
def __enter__(self):
"""Create camera on context enter."""
return self
@ -159,7 +159,7 @@ class BaseCamera(metaclass=ABCMeta):
"""Close the BaseCamera and all attached StreamObjects."""
logging.info("Closing {}".format(self))
# Close all StreamObjects
for capture_list in [self.images, self.videos]:
for capture_list in [self.images.values(), self.videos.values()]:
for stream_object in capture_list:
stream_object.close()
# Empty temp directory
@ -178,6 +178,9 @@ class BaseCamera(metaclass=ABCMeta):
shutil.rmtree(self.paths["temp"])
logging.debug("Cleared {}.".format(self.paths["temp"]))
def rebuild_captures(self):
self.images = build_captures_from_exif(self.paths["default"])
# START AND STOP WORKER THREAD
def start_worker(self, timeout: int = 5) -> bool:
@ -187,7 +190,7 @@ class BaseCamera(metaclass=ABCMeta):
self.last_access = time.time()
self.stop = False
if not self.status["stream_active"]:
if not self.stream_active:
# start background frame thread
self.thread = threading.Thread(target=self._thread)
self.thread.daemon = True
@ -207,12 +210,12 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout
if self.status["stream_active"]:
if self.stream_active:
self.stop = True
self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting for stream thread to exit.")
while self.status["stream_active"]:
while self.stream_active:
if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.")
raise TimeoutError("Timeout waiting for worker thread close.")
@ -242,20 +245,22 @@ class BaseCamera(metaclass=ABCMeta):
@property
def image(self):
"""Return the latest captured image."""
return last_entry(self.images)
return last_entry(self.images.values())
@property
def video(self):
"""Return the latest recorded video."""
return last_entry(self.videos)
return last_entry(self.videos.values())
def image_from_id(self, image_id):
"""Return an image StreamObject with a matching ID."""
return entry_by_uuid(image_id, self.images)
logging.warning("image_from_id is deprecated. Access captures as a dictionary.")
return entry_by_uuid(image_id, self.images.values())
def video_from_id(self, video_id):
"""Return a video StreamObject with a matching ID."""
return entry_by_uuid(video_id, self.videos)
logging.warning("video_from_id is deprecated. Access captures as a dictionary.")
return entry_by_uuid(video_id, self.videos.values())
# CREATING NEW CAPTURES
@ -280,7 +285,7 @@ class BaseCamera(metaclass=ABCMeta):
# Generate file name
if not filename:
filename = generate_numbered_basename(self.images)
filename = generate_numbered_basename(self.images.values())
logging.debug(filename)
filename = "{}.{}".format(filename, fmt)
@ -298,7 +303,9 @@ class BaseCamera(metaclass=ABCMeta):
output.put_tags(["temporary"])
# Update capture list
self.images.append(output)
capture_key = str(output.id)
logging.debug(f"Adding image {output} with key {capture_key}")
self.images[capture_key] = output
return output
@ -320,10 +327,11 @@ class BaseCamera(metaclass=ABCMeta):
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)
filename = generate_numbered_basename(self.videos.values())
logging.debug(filename)
filename = "{}.{}".format(filename, fmt)
@ -341,7 +349,9 @@ class BaseCamera(metaclass=ABCMeta):
output.put_tags(["temporary"])
# Update capture list
self.videos.append(output)
capture_key = str(output.id)
logging.debug(f"Adding video {output} with key {capture_key}")
self.videos[capture_key] = output
return output
@ -352,7 +362,7 @@ class BaseCamera(metaclass=ABCMeta):
self.frames_iterator = self.frames()
logging.debug("Entering worker thread.")
self.status["stream_active"] = True
self.stream_active = True
for frame in self.frames_iterator:
self.frame = frame
@ -365,9 +375,7 @@ class BaseCamera(metaclass=ABCMeta):
and ( # If using timeout
time.time() - self.last_access > self.stream_timeout
)
and not self.status[ # And timeout time
"preview_active"
] # And GPU preview is not active
and not self.preview_active # And GPU preview is not active
):
self.frames_iterator.close()
break
@ -383,4 +391,4 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state
self.status["stream_active"] = False
self.stream_active = False

View file

@ -4,12 +4,14 @@ import os
import shutil
import glob
import datetime
import yaml
import json
import logging
from PIL import Image
import dateutil.parser
import atexit
from collections import OrderedDict
from openflexure_microscope.camera import piexif
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
from openflexure_microscope.config import JSONEncoder
@ -34,11 +36,9 @@ def pull_usercomment_dict(filepath):
try:
return json.loads(exif_dict["Exif"][37510].decode())
except json.decoder.JSONDecodeError:
# TODO: Remove YAML support in a later version
logging.warning(
f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON."
logging.error(
f"Capture {filepath} has old, corrupt, or missing OpenFlexure metadata. Unable to reload to server."
)
return yaml.load(exif_dict["Exif"][37510].decode())
else:
return None
@ -60,14 +60,15 @@ def build_captures_from_exif(capture_path):
logging.debug("Reloading captures from {}...".format(capture_path))
files = make_file_list(capture_path, EXIF_FORMATS)
captures = []
captures = OrderedDict()
for f in files:
logging.debug("Reloading capture {}...".format(f))
exif = pull_usercomment_dict(f)
if exif:
capture = capture_from_exif(f, exif)
captures.append(capture)
if capture:
captures[capture.id] = capture
else:
logging.error("Invalid data at {}. Skipping.".format(f))
@ -93,18 +94,24 @@ def capture_from_exif(path, exif_dict):
# Build file path information
capture.split_file_path(capture.file)
# Populate capture parameters
capture.id = exif_dict["id"]
capture.timestring = exif_dict["time"]
capture.format = exif_dict["format"]
# Image metadata
try:
image_metadata = exif_dict.pop("image")
except KeyError as e:
logging.error(
f"Unable to obtain valid 2.0 OpenFlexure metadata from file {path}"
)
return None
capture.custom_metadata = (
exif_dict["custom"] if "custom" in exif_dict.keys() else {}
)
capture.system_metadata = (
exif_dict["system"] if "system" in exif_dict.keys() else {}
)
capture.tags = exif_dict["tags"]
# Populate capture parameters
capture.id = image_metadata.get("id")
capture.datetime = dateutil.parser.isoparse(image_metadata.get("acquisitionDate"))
capture.format = image_metadata.get("format")
capture.tags = image_metadata.get("tags")
capture.annotations = image_metadata.get("annotations")
# Since we popped the "image" key, we dump whatever is left in _metadata
capture._metadata = exif_dict
return capture
@ -113,16 +120,6 @@ class CaptureObject(object):
"""
StreamObject used to store and process on-disk capture data, and metadata.
Serves to simplify modifying properties of on-disk capture data.
Attributes:
timestring (str): Timestring of capture creation time
custom_metadata (dict): Dictionary of custom metadata to be included in metadata file
tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
filefolder (str): Folder in which the capture file will be stored
filename (str): Full name of the capture file
basename (str): Filename of the capture, without a file extension
format (str): Format of the capture data
"""
def __init__(self, filepath) -> None:
@ -131,17 +128,20 @@ class CaptureObject(object):
# Store a nice ID
self.id = uuid.uuid4() #: str: Unique capture ID
logging.debug("Created StreamObject {}".format(self.id))
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
self.datetime = datetime.datetime.now()
# Create file name. Default to UUID
self.file = filepath
self.split_file_path(self.file)
# Dictionary for storing custom metadata
self.custom_metadata = {}
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
self.system_metadata = {}
if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder)
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
self._metadata = {}
# Dictionary for storing custom annotations
self.annotations = {}
# List for storing tags
self.tags = []
@ -158,15 +158,11 @@ class CaptureObject(object):
Args:
filepath (str): String of the full file path, including file format extension
"""
# Split the full file path into a folder and a filename
self.filefolder, self.filename = os.path.split(filepath)
# Split the filename out from it's file extension
self.basename = os.path.splitext(self.filename)[0]
self.format = self.filename.split(".")[-1]
# Create folder and file
if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder)
# Split the full file path into a folder and a name
self.filefolder, self.name = os.path.split(filepath)
# Split the name out from it's file extension
self.basename = os.path.splitext(self.name)[0]
self.format = self.name.split(".")[-1]
@property
def exists(self) -> bool:
@ -204,17 +200,24 @@ class CaptureObject(object):
# HANDLE METADATA
def put_metadata(self, data: dict, system: bool = False) -> None:
def put_annotations(self, data: dict) -> None:
"""
Merge metadata from a passed dictionary into the capture metadata, and saves.
Merge annotations from a passed dictionary into the capture metadata, and saves.
Args:
data (dict): Dictionary of metadata to be added
"""
if system:
self.system_metadata.update(data)
else:
self.custom_metadata.update(data)
self.annotations.update(data)
self.save_metadata()
def put_metadata(self, data: dict) -> None:
"""
Merge root metadata from a passed dictionary into the capture metadata, and saves.
Args:
data (dict): Dictionary of metadata to be added
"""
self._metadata.update(data)
self.save_metadata()
def save_metadata(self) -> None:
@ -244,12 +247,15 @@ class CaptureObject(object):
and any added custom metadata and tags.
"""
d = {
"id": self.id,
"time": self.timestring,
"format": self.format,
"tags": self.tags,
"custom": self.custom_metadata,
"system": self.system_metadata,
"image": {
"id": self.id,
"name": self.name,
"acquisitionDate": self.datetime.isoformat(),
"format": self.format,
"tags": self.tags,
"annotations": self.annotations,
},
**self._metadata,
}
# Add custom metadata to dictionary
@ -262,7 +268,7 @@ class CaptureObject(object):
"""
# Create basic state dictionary
d = {"path": self.file, "filename": self.filename, "metadata": self.metadata}
d = {"path": self.file, "name": self.name, "metadata": self.metadata}
# Combined availability of data
if self.exists:

View file

@ -26,17 +26,13 @@ We override the logging settings in api.app by setting a level for PIL here.
pil_logger = logging.getLogger("PIL")
pil_logger.setLevel(logging.INFO)
# MAIN CLASS
class MockStreamer(BaseCamera):
class MissingCamera(BaseCamera):
def __init__(self):
# Run BaseCamera init
BaseCamera.__init__(self)
# Store state of PiCameraStreamer
self.status.update(
{"stream_active": False, "record_active": False, "board": None}
)
# Update config properties
self.image_resolution = (1312, 976)
self.stream_resolution = (640, 480)
@ -71,6 +67,16 @@ class MockStreamer(BaseCamera):
image.save(self.stream, format="JPEG")
@property
def configuration(self):
"""The current camera configuration."""
return {}
@property
def state(self):
"""The current read-only camera state."""
return {}
def initialisation(self):
"""Run any initialisation code when the frame iterator starts."""
pass
@ -101,7 +107,7 @@ class MockStreamer(BaseCamera):
return conf_dict
def apply_settings(self, config: dict):
def update_settings(self, config: dict):
"""
Write a config dictionary to the PiCameraStreamer config.
@ -117,7 +123,7 @@ class MockStreamer(BaseCamera):
with self.lock:
# Apply valid config params to camera object
if not self.status["record_active"]: # If not recording a video
if not self.record_active: # If not recording a video
for key, value in config.items(): # For each provided setting
if hasattr(self, key):
@ -192,7 +198,7 @@ class MockStreamer(BaseCamera):
if isinstance(output, CaptureObject):
target = output.file
else:
target = target
target = output
with self.lock:
if isinstance(target, str):

View file

@ -47,6 +47,11 @@ from .base import BaseCamera, CaptureObject
from .set_picamera_gain import set_analog_gain, set_digital_gain
from openflexure_microscope.paths import settings_file_path
from openflexure_microscope.utilities import (
serialise_array_b64,
ndarray_to_json,
json_to_ndarray,
)
# MAIN CLASS
@ -84,15 +89,8 @@ class PiCameraStreamer(BaseCamera):
picamera.PiCamera()
) #: :py:class:`picamera.PiCamera`: Picamera object
# Store status of PiCameraStreamer
self.status.update(
{
"stream_active": False,
"record_active": False,
"preview_active": False,
"board": f"picamera_{self.camera.revision}",
}
)
# Store state of PiCameraStreamer
self.preview_active = False
# Reset variable states
self.set_zoom(1.0)
@ -116,15 +114,22 @@ class PiCameraStreamer(BaseCamera):
"picamera_lst.npy"
) #: str: Path of .npy lens shading table file
# Update board identifier
self.status.update({})
# Create an empty stream
self.stream = io.BytesIO()
# Start streaming
self.start_worker()
@property
def configuration(self):
"""The current camera configuration."""
return {"board": self.camera.revision}
@property
def state(self):
"""The current read-only camera state."""
return {}
def initialisation(self):
"""Run any initialisation code when the frame iterator starts."""
pass
@ -153,10 +158,7 @@ class PiCameraStreamer(BaseCamera):
"image_resolution": self.image_resolution,
"numpy_resolution": self.numpy_resolution,
"jpeg_quality": self.jpeg_quality,
"picamera_lst_path": self.picamera_lst_path
if os.path.isfile(self.picamera_lst_path)
else None,
"picamera_settings": {},
"picamera": {},
}
)
@ -165,18 +167,22 @@ class PiCameraStreamer(BaseCamera):
try:
value = getattr(self.camera, key)
logging.debug("Reading PiCamera().{}: {}".format(key, value))
conf_dict["picamera_settings"][key] = value
conf_dict["picamera"][key] = value
except AttributeError:
logging.debug("Unable to read PiCamera attribute {}".format(key))
# Include a serialised lens shading table
if (
hasattr(self.camera, "lens_shading_table")
and getattr(self.camera, "lens_shading_table") is not None
):
conf_dict["picamera"]["lens_shading_table"] = ndarray_to_json(
getattr(self.camera, "lens_shading_table")
)
return conf_dict
def save_settings(self):
"""Save lens-shading table to disk"""
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
self.save_lens_shading_table()
def apply_settings(self, config: dict):
def update_settings(self, config: dict):
"""
Write a config dictionary to the PiCameraStreamer config.
@ -194,36 +200,37 @@ class PiCameraStreamer(BaseCamera):
with self.lock:
# Apply valid config params to Picamera object
if not self.status["record_active"]: # If not recording a video
if not self.record_active: # If not recording a video
# Pause stream while changing settings
if self.status["stream_active"]: # If stream is active
if self.stream_active: # If stream is active
logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done
# PiCamera parameters
if "picamera_settings" in config: # If new settings are given
if "picamera" in config: # If new settings are given
self.apply_picamera_settings(
config["picamera_settings"], pause_for_effect=True
config["picamera"], pause_for_effect=True
)
# Handle lens shading if camera supports it
if (
hasattr(self.camera, "lens_shading_table")
and "lens_shading_table" in config["picamera"]
):
try:
self.camera.lens_shading_table = json_to_ndarray(
config["picamera"].get("lens_shading_table")
)
except KeyError as e:
logging.error(e)
# PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting
if (key != "picamera_settings") and hasattr(self, key):
if (key != "picamera") and hasattr(self, key):
setattr(self, key, value)
# Handle lens shading if camera supports it
if ("picamera_lst_path" in config) and hasattr(
self.camera, "lens_shading_table"
):
logging.debug(
"Applying lens_shading_table from file: {}".format(
config["picamera_lst_path"]
)
)
self.apply_lens_shading_table(config["picamera_lst_path"])
# If stream was paused to update config, unpause
if paused_stream:
logging.info("Resuming stream.")
@ -292,53 +299,18 @@ class PiCameraStreamer(BaseCamera):
if pause_for_effect:
time.sleep(0.2)
def read_lens_shading_table(self):
"""
Read the current lens shading table as a numpy array, if it exists. Return None otherwise.
"""
if hasattr(self.camera, "lens_shading_table"):
return self.camera.lens_shading_table
else:
return None
def save_lens_shading_table(self):
"""
Save the current lens shading table to an .npy file, if it exists.
"""
logging.debug(self.read_lens_shading_table())
if self.read_lens_shading_table() is not None:
np.save(self.picamera_lst_path, self.read_lens_shading_table())
else:
logging.warning("Unable to save a nonexistant lens shading table")
def apply_lens_shading_table(self, lst_array_or_path):
"""
Apply a lens shading table from an .npy file, or numpy array.
Args:
lst_array_or_path: Numpy array, or path to .npy file, describing the lens-shading table
"""
if isinstance(lst_array_or_path, np.ndarray):
self.camera.lens_shading_table = lst_array_or_path
elif (type(lst_array_or_path) == str) and os.path.isfile(lst_array_or_path):
self.camera.lens_shading_table = np.load(lst_array_or_path)
else:
logging.error(
"Unsupported or missing data for camera lens_shading_table. Must be numpy ndarray, or .npy file path string. Skipping."
)
def set_zoom(self, zoom_value: float = 1.0) -> None:
"""
Change the camera zoom, handling re-centering and scaling.
"""
with self.lock:
self.status["zoom_value"] = float(zoom_value)
if self.status["zoom_value"] < 1:
self.status["zoom_value"] = 1
self.zoom_value = float(zoom_value)
if self.zoom_value < 1:
self.zoom_value = 1
# Richard's code for zooming !
fov = self.camera.zoom
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
size = 1.0 / self.status["zoom_value"]
size = 1.0 / self.zoom_value
# If the new zoom value would be invalid, move the centre to
# keep it within the camera's sensor (this is only relevant
# when zooming out, if the FoV is not centred on (0.5, 0.5)
@ -365,7 +337,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.preview.window = window
if fullscreen:
self.camera.preview.fullscreen = fullscreen
self.status["preview_active"] = True
self.preview_active = True
except picamera.exc.PiCameraMMALError as e:
logging.error(
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
@ -380,7 +352,7 @@ class PiCameraStreamer(BaseCamera):
def stop_preview(self):
"""Stop the on board GPU camera preview."""
self.camera.stop_preview()
self.status["preview_active"] = False
self.preview_active = False
def start_recording(self, output, fmt: str = "h264", quality: int = 15):
"""Start recording.
@ -398,7 +370,7 @@ class PiCameraStreamer(BaseCamera):
"""
with self.lock:
# Start recording method only if a current recording is not running
if not self.status["record_active"]:
if not self.record_active:
# Start the camera video recording on port 2
logging.info("Recording to {}".format(output))
@ -411,8 +383,8 @@ class PiCameraStreamer(BaseCamera):
quality=quality,
)
# Update status dictionary
self.status["record_active"] = True
# Update state
self.record_active = True
return output
@ -431,8 +403,8 @@ class PiCameraStreamer(BaseCamera):
self.camera.stop_recording(splitter_port=2)
logging.info("Recording stopped")
# Update status dictionary
self.status["record_active"] = False
# Update state
self.record_active = False
def stop_stream_recording(
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
@ -500,7 +472,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.resolution = resolution
# If the stream should be active
if self.status["stream_active"]:
if self.stream_active:
try:
# Start recording on stream port
self.camera.start_recording(
@ -551,7 +523,7 @@ class PiCameraStreamer(BaseCamera):
if isinstance(output, CaptureObject):
target = output.file
else:
target = target
target = output
with self.lock:
logging.info("Capturing to {}".format(output))
@ -680,7 +652,6 @@ class PiCameraStreamer(BaseCamera):
# Start stream recording (and set resolution)
self.start_stream_recording()
# Update status
logging.debug("STREAM ACTIVE")
# While the iterator is not closed

View file

@ -1,4 +1,5 @@
import json
import flask
import os
import errno
import logging
@ -7,7 +8,12 @@ from uuid import UUID
import numpy as np
from fractions import Fraction
from .paths import OPENFLEXURE_ETC_PATH, CONFIG_FILE_PATH, DEFAULT_CONFIG_FILE_PATH
from .paths import (
SETTINGS_FILE_PATH,
DEFAULT_SETTINGS_FILE_PATH,
CONFIGURATION_FILE_PATH,
DEFAULT_CONFIGURATION_FILE_PATH,
)
class OpenflexureSettingsFile:
@ -19,21 +25,21 @@ class OpenflexureSettingsFile:
expand (bool): Expand paths to valid auxillary config files.
"""
def __init__(self, config_path: str = None):
global DEFAULT_CONFIG, CONFIG_FILE_PATH
def __init__(self, path: str, defaults: dict = {}):
global DEFAULT_SETTINGS
# Set arguments
self.config_path = config_path or CONFIG_FILE_PATH
self.path = path
# Initialise basic config file with defaults if it doesn't exist
initialise_file(self.config_path, populate=DEFAULT_CONFIG)
initialise_file(self.path, populate=defaults)
def load(self) -> dict:
"""
Loads settings from a file on-disk.
"""
# Unexpanded config dictionary (used at load/save time)
loaded_config = load_json_file(self.config_path)
loaded_config = load_json_file(self.path)
logging.debug("Reading settings from disk")
return loaded_config
@ -50,11 +56,11 @@ class OpenflexureSettingsFile:
save_settings = config
if backup:
if os.path.isfile(self.config_path):
shutil.copyfile(self.config_path, self.config_path + ".bk")
if os.path.isfile(self.path):
shutil.copyfile(self.path, self.path + ".bk")
logging.debug("Saving settings dictionary to disk")
save_json_file(self.config_path, save_settings)
save_json_file(self.path, save_settings)
def merge(self, config: dict) -> dict:
"""
@ -71,7 +77,7 @@ class OpenflexureSettingsFile:
return settings
class JSONEncoder(json.JSONEncoder):
class JSONEncoder(flask.json.JSONEncoder):
"""
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
"""
@ -88,14 +94,13 @@ class JSONEncoder(json.JSONEncoder):
# 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
try:
return json.JSONEncoder.default(self, o)
# if it's some mystery object, just return a string representation
except TypeError:
return str(o)
return flask.json.JSONEncoder.default(self, o)
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
@ -178,10 +183,22 @@ def initialise_file(config_path, populate: str = "{}\n"):
outfile.write(populate)
# Load the default config
with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc:
DEFAULT_CONFIG = default_rc.read()
# 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(config_path=CONFIG_FILE_PATH)
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()
#: Default user settings object
user_configuration = OpenflexureSettingsFile(
path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION
)

View file

@ -6,15 +6,21 @@ import logging
import pkg_resources
import uuid
from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.stage.mock import MockStage
from openflexure_microscope.camera.base import BaseCamera
from openflexure_microscope.camera.mock import MockStreamer
from openflexure_microscope.stage.mock import MissingStage
from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.stage.sanga import SangaStage
try:
from openflexure_microscope.camera.pi import PiCameraStreamer
except ImportError:
logging.warning("Unable to import PiCameraStreamer")
from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.utilities import serialise_array_b64
from openflexure_microscope.config import user_settings
from openflexure_microscope.config import user_settings, user_configuration
from labthings.core.lock import CompositeLock
from labthings.core.utilities import rupdate
class Microscope:
@ -24,20 +30,26 @@ class Microscope:
The camera and stage objects may already be initialised, and can be passed as arguments.
"""
def __init__(self):
# Initial attributes
self.id = uuid.uuid4() #: Microscope UUID
self.name = self.id #: Microscope name (modifiable)
self.fov = [0, 0] #: Microscope field-of-view in stage motor steps
self.camera = None #: Currently connected camera object
self.stage = None #: Currently connected stage object
def __init__(self, settings=user_settings, configuration=user_configuration):
self.id = uuid.uuid4()
self.name = self.id
self.fov = [0, 0] #: Microscope field-of-view in stage motor steps
# Store settings and configuration files
self.settings_file = settings
self.configuration_file = configuration
# Initialise with an empty composite lock
#: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage
self.lock = CompositeLock([])
self.camera = None #: Currently connected camera object
self.stage = None #: Currently connected stage object
self.setup(self.configuration_file.load()) # Attach components
# Apply settings loaded from file
self.apply_settings(user_settings.load())
self.update_settings(self.settings_file.load())
def __enter__(self):
"""Create microscope on context enter."""
@ -56,58 +68,49 @@ class Microscope:
self.stage.close()
logging.info("Closed {}".format(self))
def attach(self, camera: BaseCamera, stage: BaseStage):
def setup(self, configuration):
"""
Retroactively attaches a camera and stage to the microscope object.
Allows the microscope to be created as a "dummy", with hardware communications
opened at a later time.
Args:
camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): camera object
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object
Attach microscope components based on initially passed configuration file
"""
settings_full = self.read_settings()
### Detector
if configuration.get("camera"):
camera_type = configuration["camera"].get("type")
if camera_type in ("PiCamera", "PiCameraStreamer"):
try:
self.camera = PiCameraStreamer()
except Exception as e:
logging.error(e)
logging.warning("No compatible camera hardware found.")
logging.debug("Attaching camera...")
self.camera = (
camera
) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object
### Stage
if configuration.get("stage"):
stage_type = configuration["stage"].get("type")
stage_port = configuration["stage"].get("port")
if stage_type in ("SangaBoard", "SangaStage"):
try:
self.stage = SangaStage(port=stage_port)
except Exception as e:
logging.error(e)
logging.warning("No compatible Sangaboard hardware found.")
### Fallbacks
if not self.camera:
logging.info("No camera attached.")
else:
logging.info("Attached camera {}".format(camera))
if hasattr(self.camera, "lock"): # If camera has a lock
logging.info("Attaching {} to composite lock.".format(self.camera.lock))
# Add the lock to the microscope composite lock
self.lock.locks.append(self.camera.lock)
logging.debug("Attaching stage...")
self.stage = (
stage
) #: :py:class:`openflexure_microscope.stage.base.BaseStage`: OpenFlexure stage object
self.camera = MissingCamera()
if not self.stage:
logging.info("No stage attached.")
else:
logging.info("Attached stage {}".format(stage))
self.stage = MissingStage()
if hasattr(self.stage, "lock"): # If stage object has a lock
logging.info(
"Attaching lock {} to composite lock.".format(self.stage.lock)
)
# Add the lock to the microscope composite lock
self.lock.locks.append(self.stage.lock)
logging.info("Reapplying settings to newly attached devices")
self.apply_settings(settings_full)
### Locks
if hasattr(self.camera, "lock"):
self.lock.locks.append(self.camera.lock)
if hasattr(self.stage, "lock"):
self.lock.locks.append(self.stage.lock)
def has_real_stage(self) -> bool:
"""
Check if a real (non-mock) stage is currently attached.
"""
if hasattr(self, "stage") and not isinstance(self.stage, MockStage):
if hasattr(self, "stage") and not isinstance(self.stage, MissingStage):
return True
else:
return False
@ -116,49 +119,45 @@ class Microscope:
"""
Check if a real (non-mock) camera is currently attached.
"""
if hasattr(self, "camera") and not isinstance(self.camera, MockStreamer):
if hasattr(self, "camera") and not isinstance(self.camera, MissingCamera):
return True
else:
return False
# Create unified status
# Create unified state
@property
def status(self):
"""Dictionary of the basic microscope status.
def state(self):
"""Dictionary of the basic microscope state.
Return:
dict: Dictionary containing complete microscope status
dict: Dictionary containing complete microscope state
"""
state = {
"camera": self.camera.status,
"stage": self.stage.status,
"version": pkg_resources.get_distribution("openflexure_microscope").version,
}
state = {"camera": self.camera.state, "stage": self.stage.state}
return state
def apply_settings(self, config: dict):
def update_settings(self, settings: dict):
"""
Applies a settings dictionary to the microscope. Missing parameters will be left untouched.
"""
logging.debug("Microscope: Applying config: {}".format(config))
logging.debug("Microscope: Applying settings: {}".format(settings))
# If attached to a camera
if ("camera_settings" in config) and self.camera:
self.camera.apply_settings(config["camera_settings"])
if ("camera" in settings) and self.camera:
self.camera.update_settings(settings["camera"])
# If attached to a stage
if ("stage_settings" in config) and self.stage:
self.stage.apply_settings(config["stage_settings"])
if ("stage" in settings) and self.stage:
self.stage.update_settings(settings["stage"])
# Todo: tidy up with some loopy goodness
if "id" in config:
self.id = config["id"]
if "name" in config:
self.name = config["name"]
if "fov" in config:
self.fov = config["fov"]
# Microscope settings
if "id" in settings:
self.id = settings["id"]
if "name" in settings:
self.name = settings["name"]
if "fov" in settings:
self.fov = settings["fov"]
def read_settings(self, json_safe=False):
def read_settings(self, full: bool = True):
"""
Get an updated settings dictionary.
@ -174,16 +173,34 @@ class Microscope:
# If attached to a camera
if self.camera:
settings_current_camera = self.camera.read_settings()
settings_current["camera_settings"] = settings_current_camera
settings_current["camera"] = settings_current_camera
# Store an encoded copy of the PiCamera lens shading table, if it exists
if hasattr(self.camera, "read_lens_shading_table"):
# Read LST. Returns None if no LST is active
lst_arr = self.camera.read_lens_shading_table()
if lst_arr is not None:
b64_string, dtype, shape = serialise_array_b64(lst_arr)
settings_current["camera"]["lens_shading_table"] = {
"@type": "ndarray",
"b64_string": b64_string,
"dtype": dtype,
"shape": shape,
}
# If attached to a stage
if self.stage:
settings_current_stage = self.stage.read_settings()
settings_current["stage_settings"] = settings_current_stage
settings_current["stage"] = settings_current_stage
settings_full = user_settings.merge(settings_current)
settings_full = self.settings_file.merge(settings_current)
return settings_full
if full:
return settings_full
else:
return settings_current
def save_settings(self):
"""
@ -192,11 +209,31 @@ class Microscope:
# Read curent config
current_config = self.read_settings()
# Save config to file
if self.camera:
self.camera.save_settings()
if self.stage:
self.stage.save_settings()
user_settings.save(current_config, backup=True)
self.settings_file.save(current_config, backup=True)
@property
def configuration(self):
initial_configuration = self.configuration_file.load()
current_configuration = {
"application": {
"name": "openflexure_microscope",
"version": pkg_resources.get_distribution(
"openflexure_microscope"
).version,
},
"stage": {
"type": self.stage.__class__.__name__,
**self.stage.configuration,
},
"camera": {
"type": self.camera.__class__.__name__,
**self.camera.configuration,
},
}
initial_configuration.update(current_configuration)
return initial_configuration
@property
def metadata(self):
@ -204,23 +241,10 @@ class Microscope:
Microscope system metadata, to be applied to basically all captures
"""
system_metadata = {
"microscope_settings": self.read_settings(),
"microscope_state": self.status,
"microscope_id": self.id,
"microscope_name": self.name,
"id": self.id,
"settings": self.read_settings(full=False),
"state": self.state,
"configuration": self.configuration,
}
# Store an encoded copy of the PiCamera lens shading table, if it exists
if self.camera and hasattr(self.camera, "read_lens_shading_table"):
# Read LST. Returns None if no LST is active
lst_arr = self.camera.read_lens_shading_table()
b64_string, dtype, shape = serialise_array_b64(lst_arr)
system_metadata["lens_shading_table"] = {
"b64_string": b64_string,
"dtype": dtype,
"shape": shape,
}
return system_metadata

View file

@ -0,0 +1,9 @@
{
"camera": {
"type": "PiCamera"
},
"stage": {
"type": "SangaStage",
"port": null
}
}

View file

@ -3,5 +3,5 @@
4100,
3146
],
"jpeg_quality": 75
"jpeg_quality": 100
}

View file

@ -10,12 +10,34 @@ def check_rw(path):
def settings_file_path(filename: str):
"""Generate a full file path for a filename to be stored in server settings folder"""
return os.path.join(OPENFLEXURE_ETC_PATH, filename)
settings_dir = os.path.join(OPENFLEXURE_VAR_PATH, "settings")
if not os.path.exists(settings_dir):
os.makedirs(settings_dir)
return os.path.join(settings_dir, filename)
def data_file_path(filename: str):
"""Generate a full file path for a filename to be stored in server data folder"""
return os.path.join(OPENFLEXURE_VAR_PATH, filename)
data_dir = os.path.join(OPENFLEXURE_VAR_PATH, "data")
if not os.path.exists(data_dir):
os.makedirs(data_dir)
return os.path.join(data_dir, filename)
def extensions_file_path(filename: str):
"""Generate a full file path for a folder to be stored in server extensions"""
ext_dir = os.path.join(OPENFLEXURE_VAR_PATH, "extensions")
if not os.path.exists(ext_dir):
os.makedirs(ext_dir)
return os.path.join(ext_dir, filename)
def logs_file_path(filename: str):
"""Generate a full file path for a filename to be stored in server logs"""
logs_dir = os.path.join(OPENFLEXURE_VAR_PATH, "logs")
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
return os.path.join(logs_dir, filename)
# HANDLE DEFAULTS FILES STORED IN THIS APPLICATION
@ -23,10 +45,13 @@ def data_file_path(filename: str):
HERE = os.path.abspath(os.path.dirname(__file__))
#: Path of default (first-run) microscope settings
DEFAULT_CONFIG_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json")
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"
)
# DATA BASE PATHS
# BASE PATHS
if os.name == "nt":
PREFERRED_VAR_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData"
@ -49,37 +74,11 @@ else:
OPENFLEXURE_VAR_PATH = FALLBACK_OPENFLEXURE_VAR_PATH
# SERVER BASE PATHS
if os.name == "nt":
PREFERRED_ETC_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData"
FALLBACK_ETC_PATH = os.path.expanduser("~")
else:
PREFERRED_ETC_PATH = "/etc"
FALLBACK_ETC_PATH = os.path.join(os.path.expanduser("~"), ".config")
PREFERRED_OPENFLEXURE_ETC_PATH = os.path.join(PREFERRED_ETC_PATH, "openflexure")
FALLBACK_OPENFLEXURE_ETC_PATH = os.path.join(FALLBACK_ETC_PATH, "openflexure")
if not os.path.exists(PREFERRED_OPENFLEXURE_ETC_PATH) and check_rw(PREFERRED_ETC_PATH):
os.makedirs(PREFERRED_OPENFLEXURE_ETC_PATH)
if check_rw(PREFERRED_OPENFLEXURE_ETC_PATH):
OPENFLEXURE_ETC_PATH = PREFERRED_OPENFLEXURE_ETC_PATH
else:
if not os.path.exists(FALLBACK_OPENFLEXURE_ETC_PATH):
os.makedirs(FALLBACK_OPENFLEXURE_ETC_PATH)
OPENFLEXURE_ETC_PATH = FALLBACK_OPENFLEXURE_ETC_PATH
# SERVER PATHS
#: Path of microscope settings directory
CONFIG_FILE_PATH = os.path.join(OPENFLEXURE_ETC_PATH, "microscope_settings.json")
#: Path of microscope settings file
SETTINGS_FILE_PATH = settings_file_path("microscope_settings.json")
#: Path of microscope configuration file
CONFIGURATION_FILE_PATH = settings_file_path("microscope_configuration.json")
#: Path of microscope extensions directory
OPENFLEXURE_EXTENSIONS_PATH = os.path.join(
OPENFLEXURE_ETC_PATH, "microscope_extensions"
)
# DATA PATHS
OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")

View file

@ -0,0 +1,77 @@
from openflexure_microscope.paths import (
FALLBACK_OPENFLEXURE_VAR_PATH,
PREFERRED_OPENFLEXURE_VAR_PATH,
)
import logging
import os
from . import check_settings, check_capture_reload
# Paths for suggestions
LOGS_PATHS = [
os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "logs"),
os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "logs"),
]
SETTINGS_PATHS = [
os.path.join(var_path, "settings", "microscope_settings.json")
for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH)
]
CONFIG_PATHS = [
os.path.join(var_path, "settings", "microscope_configuration.json")
for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH)
]
DATA_PATHS = [
os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "data"),
os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "data"),
]
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logging.debug("Testing debug logger. One two one two.")
class bcolors:
HEADER = "\033[95m"
OKBLUE = "\033[94m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
error_keys = {
"config_settings_import_error": "The configuration submodule could not be imported properly. This is usually because the default config or settings files are badly broken somehow. See further errors for details",
"default_config_empty": "The default configuration file is empty, and could not be automatically populated.",
"default_settings_empty": "The default settings file is empty, and could not be automatically populated.",
"default_config_error": f"The default configuration file could not be parsed. To fix, consider backing up and deleting your configuration files from {CONFIG_PATHS} to reset the configuration.",
"default_settings_error": f"The default settings file could not be parsed. To fix, consider backing up and deleting your configuration files from {SETTINGS_PATHS} to reset the configuration.",
"capture_rebuild_timeout": f"Capture database rebuilding took a long time. This may not cause catastrophic errors, but rather will cause the server to hang for a while. To fix, consider moving your captures from {DATA_PATHS} to another location.",
}
if __name__ == "__main__":
spoof = False
error_sources = []
if spoof:
error_sources = list(error_keys.keys())
error_sources.extend(check_settings.main())
error_sources.extend(check_capture_reload.main())
if not error_sources:
print()
print(bcolors.OKGREEN + "No errors found!" + bcolors.ENDC)
print(
"That's not to say everything is fine, only that our automatic diagnostics couldn't find much."
)
print(f"You can check through the server logs at {LOGS_PATHS}")
else:
for err_code in error_sources:
logging.error(err_code)
if error_keys.get(err_code):
print(bcolors.FAIL + error_keys.get(err_code) + bcolors.ENDC)

View file

@ -0,0 +1,34 @@
from openflexure_microscope.rescue.monitor_timeout import launch_timeout_test_process
from openflexure_microscope.camera.capture import build_captures_from_exif
from openflexure_microscope.camera.base import BASE_CAPTURE_PATH
from openflexure_microscope.config import user_settings
import logging
def check_capture_rebuild(timeout=10):
logging.info("Loading user settings...")
settings = user_settings.load()
cap_path = str(settings.get("camera", {}).get("paths", {}).get("default"))
logging.info(f"Capture path found: {cap_path}")
if not cap_path:
logging.error(
"No capture path defined in settings. This is unusual for anything other than a first-run. \nFalling back to default path."
)
cap_path = BASE_CAPTURE_PATH
logging.info("Starting capture reload with a 10 second timeout...")
passed_timeout_test = launch_timeout_test_process(
build_captures_from_exif, args=(cap_path,)
)
return passed_timeout_test
def main():
error_sources = []
passed_timeout = check_capture_rebuild()
if not passed_timeout:
error_sources.append("capture_rebuild_timeout")
return error_sources

View file

@ -0,0 +1,47 @@
import logging
import json
ERROR_SOURCES = []
def trace_config_exceptions():
error_sources = []
from openflexure_microscope.paths import (
DEFAULT_CONFIGURATION_FILE_PATH,
SETTINGS_FILE_PATH,
)
try:
default_config = json.load(DEFAULT_CONFIGURATION_FILE_PATH)
if not default_config:
error_sources.append("default_config_empty")
except Exception as e:
logging.error("Error parsing config:")
logging.error(e)
error_sources.append("default_config_error")
try:
default_settings = json.load(SETTINGS_FILE_PATH)
if not default_settings:
error_sources.append("default_settings_empty")
except Exception as e:
logging.error("Error parsing settings:")
logging.error(e)
error_sources.append("default_settings_error")
return error_sources
def main():
error_sources = []
logging.info("Attempting default settings and config import...")
try:
from openflexure_microscope import config
except Exception as e:
error_sources.append("config_settings_import_error")
logging.error("Error importing config:")
logging.error(e)
error_sources.extend(trace_config_exceptions())
return error_sources

View file

@ -0,0 +1,59 @@
#!/bin/python
#
# Copyright 2016 Flavio Garcia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Usage: monitor_process.py <service_name>
#
# Example(crontab, every 5 minutes):
# */5 * * * * /root/bin/monitor_service.py prosody > /dev/null 2>&1
#
import sys
import subprocess
class ServiceMonitor(object):
def __init__(self, service):
self.service = service
def is_active(self):
"""Return True if service is running"""
for line in self.status():
if "Active:" in line:
if "(running)" in line:
return True
return False
def status(self):
cmd = f"/bin/systemctl status {self.service}.service"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
stdout_list = proc.communicate()[0].decode().split("\n")
return stdout_list
def start(self):
cmd = f"/bin/systemctl start {self.service}.service"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
proc.communicate()
def stop(self):
cmd = f"/bin/systemctl stop {self.service}.service"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
proc.communicate()
def log(self, n_lines: int = 100):
cmd = f"/bin/journalctl -u {self.service}.service -n {n_lines} --no-pager"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
stdout_list = proc.communicate()[0].decode().split("\n")
return stdout_list

View file

@ -0,0 +1,39 @@
import multiprocessing
import logging
import time
# bar
def test_long_fn():
for _ in range(100):
print("Tick")
time.sleep(1)
def launch_timeout_test_process(target, args=(), kwargs=None, timeout=10):
if not kwargs:
kwargs = {}
# Start target as a process
p = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
p.start()
# Wait for 10 seconds or until process finishes
p.join(timeout)
# If thread is still active
if p.is_alive():
logging.error(
f"Function {target} reached timeout after {timeout} seconds. Terminating."
)
# Terminate
p.terminate()
p.join()
return False
else:
return True
if __name__ == "__main__":
launch_timeout_test_process(test_long_fn, timeout=5)

View file

@ -14,7 +14,7 @@ class BaseStage(metaclass=ABCMeta):
self.lock = StrictLock(timeout=5)
@abstractmethod
def apply_settings(self, config: dict):
def update_settings(self, config: dict):
"""Update settings from a config dictionary"""
pass
@ -23,18 +23,16 @@ class BaseStage(metaclass=ABCMeta):
"""Return the current settings as a dictionary"""
pass
def save_settings(self):
"""(Optional) Save any settings to disk that need to be stored"""
return
@property
@abstractmethod
def state(self):
"""The general state dictionary of the board."""
pass
@property
@abstractmethod
def status(self):
"""The general state dictionary of the board.
Should at least contain 'position', and 'board' keys.
Note: A None/Null value for 'board' will disable stage
movement in the OpenFlexure eV client software,
"""
def configuration(self):
"""The general stage configuration."""
pass
@property
@ -51,11 +49,7 @@ class BaseStage(metaclass=ABCMeta):
@property
def position_map(self):
return {
"x": self.position[0],
"y": self.position[1],
"z": self.position[2],
}
return {"x": self.position[0], "y": self.position[1], "z": self.position[2]}
@property
@abstractmethod

View file

@ -7,7 +7,7 @@ import time
import logging
class MockStage(BaseStage):
class MissingStage(BaseStage):
def __init__(self, port=None, **kwargs):
BaseStage.__init__(self)
self._position = [0, 0, 0]
@ -17,19 +17,17 @@ class MockStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property
def status(self):
"""The general status dictionary of the board."""
status = {
"position": self.position_map,
"board": None,
"firmware": None,
"version": None
}
return status
def state(self):
"""The general state dictionary of the board."""
state = {"position": self.position_map}
return state
def apply_settings(self, config: dict):
@property
def configuration(self):
return {}
def update_settings(self, config: dict):
"""Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels
if "backlash" in config:
# Construct backlash array

View file

@ -23,6 +23,7 @@ class SangaStage(BaseStage):
"""Class managing serial communications with the motors for an Openflexure stage"""
BaseStage.__init__(self)
self.port = port
self.board = Sangaboard(port, **kwargs)
self._backlash = (
@ -31,14 +32,17 @@ class SangaStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property
def status(self):
"""The general status dictionary of the board."""
status = {
"position": self.position_map,
def state(self):
"""The general state dictionary of the board."""
return {"position": self.position_map}
@property
def configuration(self):
return {
"port": self.port,
"board": self.board.board,
"firmware": self.board.firmware,
}
return status
@property
def n_axes(self):
@ -81,7 +85,7 @@ class SangaStage(BaseStage):
else:
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
def apply_settings(self, config: dict):
def update_settings(self, config: dict):
"""Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels

View file

@ -4,6 +4,7 @@ import operator
import base64
from uuid import UUID
import numpy as np
import logging
from collections import abc
from functools import reduce
from contextlib import contextmanager
@ -21,6 +22,31 @@ def serialise_array_b64(npy_arr):
return b64_string, dtype, shape
def ndarray_to_json(arr: np.ndarray):
if isinstance(arr, memoryview):
# We can transparently convert memoryview objects to arrays
# This comes in very handy for the lens shading table.
arr = np.array(arr)
b64_string, dtype, shape = serialise_array_b64(arr)
return {
"@type": "ndarray",
"dtype": dtype,
"shape": shape,
"base64": b64_string
}
def json_to_ndarray(json_dict: dict):
if not json_dict.get("@type") != "ndarray":
logging.warning("No valid @type attribute found. Conversion may fail.")
for required_param in ("dtype", "shape", "base64"):
if not json_dict.get(required_param):
raise KeyError(f"Missing required key {required_param}")
return deserialise_array_b64(json_dict.get("base64"), json_dict.get("dtype"), json_dict.get("shape"))
@contextmanager
def set_properties(obj, **kwargs):
"""A context manager to set, then reset, certain properties of an object.

518
poetry.lock generated
View file

@ -12,12 +12,12 @@ description = "A pluggable API specification generator. Currently supports the O
name = "apispec"
optional = false
python-versions = ">=3.5"
version = "3.2.0"
version = "3.3.0"
[package.extras]
dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)", "tox"]
docs = ["marshmallow (>=2.19.2)", "pyyaml (5.2)", "sphinx (2.3.0)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"]
lint = ["flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)"]
dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"]
docs = ["marshmallow (>=2.19.2)", "pyyaml (5.3)", "sphinx (2.4.1)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"]
lint = ["flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
tests = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"]
validation = ["prance (>=0.11)"]
yaml = ["PyYAML (>=3.10)"]
@ -97,6 +97,18 @@ optional = false
python-versions = "*"
version = "2019.11.28"
[[package]]
category = "main"
description = "Foreign Function Interface for Python calling C code."
marker = "sys_platform == \"win32\" and platform_python_implementation == \"CPython\""
name = "cffi"
optional = false
python-versions = "*"
version = "1.14.0"
[package.dependencies]
pycparser = "*"
[[package]]
category = "dev"
description = "Universal encoding detector for Python 2 and 3"
@ -110,8 +122,8 @@ category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "7.0"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.1"
[[package]]
category = "dev"
@ -161,13 +173,51 @@ version = "3.0.8"
Flask = ">=0.9"
Six = "*"
[[package]]
category = "main"
description = "Coroutine-based network library"
name = "gevent"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.4.0"
[package.dependencies]
cffi = ">=1.11.5"
greenlet = ">=0.4.14"
[package.extras]
dnspython = ["dnspython", "idna"]
doc = ["repoze.sphinx.autointerface"]
events = ["zope.event", "zope.interface"]
test = ["zope.interface", "zope.event", "requests", "objgraph", "psutil", "futures", "mock", "coverage (>=5.0a3)", "coveralls (>=1.0)"]
[[package]]
category = "main"
description = "Websocket handler for the gevent pywsgi server, a Python network library"
name = "gevent-websocket"
optional = false
python-versions = "*"
version = "0.10.1"
[package.dependencies]
gevent = "*"
[[package]]
category = "main"
description = "Lightweight in-process concurrent programming"
marker = "platform_python_implementation == \"CPython\""
name = "greenlet"
optional = false
python-versions = "*"
version = "0.4.15"
[[package]]
category = "dev"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.8"
version = "2.9"
[[package]]
category = "dev"
@ -204,8 +254,8 @@ category = "main"
description = "A very fast and expressive template engine."
name = "jinja2"
optional = false
python-versions = "*"
version = "2.10.3"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.11.1"
[package.dependencies]
MarkupSafe = ">=0.23"
@ -215,23 +265,21 @@ i18n = ["Babel (>=0.8)"]
[[package]]
category = "main"
description = ""
description = "Python implementation of LabThings, based on the Flask microframework"
name = "labthings"
optional = false
python-versions = "^3.6"
version = "0.1.0"
python-versions = ">=3.6,<4.0"
version = "0.2.0"
[package.dependencies]
Flask = "^1.1.1"
apispec = "^3.2.0"
flask-cors = "^3.0.8"
marshmallow = "^3.3.0"
webargs = "^5.5.2"
Flask = ">=1.1.1,<2.0.0"
apispec = ">=3.2.0,<4.0.0"
flask-cors = ">=3.0.8,<4.0.0"
gevent = ">=1.4.0,<2.0.0"
gevent-websocket = ">=0.10.1,<0.11.0"
marshmallow = ">=3.4.0,<4.0.0"
webargs = ">=5.5.3,<6.0.0"
[package.source]
reference = "ea5142a9642e7b852a6e1175f3c4aa1a4bfac044"
type = "git"
url = "https://github.com/labthings/python-labthings.git"
[[package]]
category = "dev"
description = "A fast and thorough lazy object proxy."
@ -254,12 +302,12 @@ description = "A lightweight library for converting complex datatypes to and fro
name = "marshmallow"
optional = false
python-versions = ">=3.5"
version = "3.3.0"
version = "3.5.1"
[package.extras]
dev = ["pytest", "pytz", "simplejson", "mypy (0.750)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)", "tox"]
docs = ["sphinx (2.2.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"]
lint = ["mypy (0.750)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)"]
dev = ["pytest", "pytz", "simplejson", "mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"]
docs = ["sphinx (2.4.3)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"]
lint = ["mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]]
@ -276,7 +324,7 @@ description = "NumPy is the fundamental package for array computing with Python.
name = "numpy"
optional = false
python-versions = ">=3.5"
version = "1.18.1"
version = "1.18.2"
[[package]]
category = "dev"
@ -284,7 +332,7 @@ description = "Core utilities for Python packages"
name = "packaging"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.0"
version = "20.3"
[package.dependencies]
pyparsing = ">=2.0.2"
@ -296,7 +344,7 @@ description = "A pure Python interface for the Raspberry Pi camera module."
name = "picamera"
optional = true
python-versions = "*"
version = "1.13.1b0"
version = "1.13.2b0"
[package.extras]
array = ["numpy"]
@ -304,7 +352,7 @@ doc = ["sphinx"]
test = ["coverage", "pytest", "mock", "pillow", "numpy"]
[package.source]
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
reference = "79177fa7f28d6d5c6eab5ba814b420b1785b6b24"
type = "git"
url = "https://github.com/rwb27/picamera.git"
[[package]]
@ -315,13 +363,33 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.4.1"
[[package]]
category = "main"
description = "Cross-platform lib for process and system monitoring in Python."
name = "psutil"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.7.0"
[package.extras]
enum = ["enum34"]
[[package]]
category = "main"
description = "C parser in Python"
marker = "sys_platform == \"win32\" and platform_python_implementation == \"CPython\""
name = "pycparser"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.20"
[[package]]
category = "dev"
description = "Pygments is a syntax highlighting package written in Python."
name = "pygments"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.5.2"
python-versions = ">=3.5"
version = "2.6.1"
[[package]]
category = "dev"
@ -353,6 +421,17 @@ optional = false
python-versions = "*"
version = "3.4"
[[package]]
category = "main"
description = "Extensions to the standard Python datetime module"
name = "python-dateutil"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
version = "2.8.1"
[package.dependencies]
six = ">=1.5"
[[package]]
category = "dev"
description = "World timezone definitions, modern and historical"
@ -361,30 +440,22 @@ optional = false
python-versions = "*"
version = "2019.3"
[[package]]
category = "main"
description = "YAML parser and emitter for Python"
name = "pyyaml"
optional = false
python-versions = "*"
version = "3.13"
[[package]]
category = "dev"
description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.22.0"
version = "2.23.0"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<3.1.0"
idna = ">=2.5,<2.9"
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"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]]
@ -414,6 +485,14 @@ version = "1.3.0"
[package.dependencies]
numpy = ">=1.13.3"
[[package]]
category = "main"
description = "Simple, fast, extensible JSON encoder/decoder for Python"
name = "simplejson"
optional = false
python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*"
version = "3.17.0"
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
@ -436,7 +515,7 @@ description = "Python documentation generator"
name = "sphinx"
optional = false
python-versions = ">=3.5"
version = "2.3.1"
version = "2.4.4"
[package.dependencies]
Jinja2 = ">=2.3"
@ -459,40 +538,43 @@ sphinxcontrib-serializinghtml = "*"
[package.extras]
docs = ["sphinxcontrib-websupport"]
test = ["pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"]
test = ["pytest (<5.3.3)", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"]
[[package]]
category = "dev"
description = ""
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
name = "sphinxcontrib-applehelp"
optional = false
python-versions = "*"
version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
name = "sphinxcontrib-devhelp"
optional = false
python-versions = "*"
version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
name = "sphinxcontrib-htmlhelp"
optional = false
python-versions = "*"
python-versions = ">=3.5"
version = "1.0.2"
[package.extras]
test = ["pytest", "flake8", "mypy", "html5lib"]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
category = "dev"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
name = "sphinxcontrib-devhelp"
optional = false
python-versions = ">=3.5"
version = "1.0.2"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
category = "dev"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
name = "sphinxcontrib-htmlhelp"
optional = false
python-versions = ">=3.5"
version = "1.0.3"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest", "html5lib"]
[[package]]
category = "dev"
@ -519,25 +601,27 @@ test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
name = "sphinxcontrib-qthelp"
optional = false
python-versions = "*"
version = "1.0.2"
python-versions = ">=3.5"
version = "1.0.3"
[package.extras]
test = ["pytest", "flake8", "mypy"]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
category = "dev"
description = ""
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
name = "sphinxcontrib-serializinghtml"
optional = false
python-versions = "*"
version = "1.1.3"
python-versions = ">=3.5"
version = "1.1.4"
[package.extras]
test = ["pytest", "flake8", "mypy"]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
category = "dev"
@ -561,8 +645,8 @@ category = "dev"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
version = "1.25.7"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.8"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
@ -575,10 +659,11 @@ description = "Declarative parsing and validation of HTTP request objects, with
name = "webargs"
optional = false
python-versions = "*"
version = "5.5.2"
version = "5.5.3"
[package.dependencies]
marshmallow = ">=2.15.2"
simplejson = ">=2.1.0"
[package.extras]
dev = ["pytest", "mock", "webtest (2.0.33)", "Flask (>=0.12.2)", "Django (>=1.11.16)", "bottle (>=0.12.13)", "tornado (>=4.5.2)", "pyramid (>=1.9.1)", "webapp2 (>=3.0.0b1)", "falcon (>=1.4.0,<2.0)", "flake8 (3.7.8)", "pre-commit (>=1.17,<2.0)", "tox", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "aiohttp (>=3.0.0)", "mypy (0.730)", "flake8-bugbear (19.8.0)"]
@ -592,12 +677,11 @@ category = "main"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.16.0"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.0.0"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
termcolor = ["termcolor"]
watchdog = ["watchdog"]
[[package]]
@ -612,7 +696,7 @@ version = "1.11.2"
rpi = ["picamera", "RPi.GPIO"]
[metadata]
content-hash = "53c82daa835ef29f7aac8ac7d36048487a9d989662a58a2d05d9a70465fbc8c6"
content-hash = "3a3a3bd4ffd7e0fe4c4c418a1b191c33afd375e6f06bb298189fd84ff75ee4cc"
python-versions = "^3.6"
[metadata.files]
@ -621,8 +705,8 @@ alabaster = [
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
]
apispec = [
{file = "apispec-3.2.0-py2.py3-none-any.whl", hash = "sha256:f2ecb3a6534cfb42cf69b5c1222d2c0e695aab5ec3d7edde31b7354670948f05"},
{file = "apispec-3.2.0.tar.gz", hash = "sha256:c1625ae910f699c9adb21928693a3245b9faab6843f1b1c94ab2d505549cd78a"},
{file = "apispec-3.3.0-py2.py3-none-any.whl", hash = "sha256:9bf4e51d56c9067c60668b78210ae213894f060f85593dc2ad8805eb7d875a2a"},
{file = "apispec-3.3.0.tar.gz", hash = "sha256:419d0564b899e182c2af50483ea074db8cb05fee60838be58bb4542095d5c08d"},
]
appdirs = [
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
@ -648,13 +732,43 @@ certifi = [
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
{file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
]
cffi = [
{file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"},
{file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"},
{file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"},
{file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"},
{file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"},
{file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"},
{file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"},
{file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"},
{file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"},
{file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"},
{file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"},
{file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"},
{file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"},
{file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"},
{file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"},
{file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"},
]
chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
]
click = [
{file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"},
{file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"},
{file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
{file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
]
colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
@ -672,9 +786,62 @@ flask-cors = [
{file = "Flask-Cors-3.0.8.tar.gz", hash = "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16"},
{file = "Flask_Cors-3.0.8-py2.py3-none-any.whl", hash = "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"},
]
gevent = [
{file = "gevent-1.4.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917"},
{file = "gevent-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8"},
{file = "gevent-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea"},
{file = "gevent-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee"},
{file = "gevent-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c"},
{file = "gevent-1.4.0-cp34-cp34m-macosx_10_14_x86_64.whl", hash = "sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64"},
{file = "gevent-1.4.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05"},
{file = "gevent-1.4.0-cp34-cp34m-win32.whl", hash = "sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12"},
{file = "gevent-1.4.0-cp34-cp34m-win_amd64.whl", hash = "sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1"},
{file = "gevent-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922"},
{file = "gevent-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e"},
{file = "gevent-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8"},
{file = "gevent-1.4.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0"},
{file = "gevent-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c"},
{file = "gevent-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1"},
{file = "gevent-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad"},
{file = "gevent-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950"},
{file = "gevent-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942"},
{file = "gevent-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51"},
{file = "gevent-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e"},
{file = "gevent-1.4.0-pp260-pypy_41-macosx_10_14_x86_64.whl", hash = "sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51"},
{file = "gevent-1.4.0-pp260-pypy_41-win32.whl", hash = "sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909"},
{file = "gevent-1.4.0.tar.gz", hash = "sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1"},
]
gevent-websocket = [
{file = "gevent-websocket-0.10.1.tar.gz", hash = "sha256:7eaef32968290c9121f7c35b973e2cc302ffb076d018c9068d2f5ca8b2d85fb0"},
{file = "gevent_websocket-0.10.1-py3-none-any.whl", hash = "sha256:17b67d91282f8f4c973eba0551183fc84f56f1c90c8f6b6b30256f31f66f5242"},
]
greenlet = [
{file = "greenlet-0.4.15-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163"},
{file = "greenlet-0.4.15-cp27-cp27m-win32.whl", hash = "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87"},
{file = "greenlet-0.4.15-cp27-cp27m-win_amd64.whl", hash = "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638"},
{file = "greenlet-0.4.15-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248"},
{file = "greenlet-0.4.15-cp33-cp33m-win32.whl", hash = "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720"},
{file = "greenlet-0.4.15-cp33-cp33m-win_amd64.whl", hash = "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28"},
{file = "greenlet-0.4.15-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939"},
{file = "greenlet-0.4.15-cp34-cp34m-win32.whl", hash = "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625"},
{file = "greenlet-0.4.15-cp34-cp34m-win_amd64.whl", hash = "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490"},
{file = "greenlet-0.4.15-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0"},
{file = "greenlet-0.4.15-cp35-cp35m-win32.whl", hash = "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4"},
{file = "greenlet-0.4.15-cp35-cp35m-win_amd64.whl", hash = "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6"},
{file = "greenlet-0.4.15-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8"},
{file = "greenlet-0.4.15-cp36-cp36m-win32.whl", hash = "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0"},
{file = "greenlet-0.4.15-cp36-cp36m-win_amd64.whl", hash = "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"},
{file = "greenlet-0.4.15-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214"},
{file = "greenlet-0.4.15-cp37-cp37m-win32.whl", hash = "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043"},
{file = "greenlet-0.4.15-cp37-cp37m-win_amd64.whl", hash = "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304"},
{file = "greenlet-0.4.15-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef"},
{file = "greenlet-0.4.15-cp38-cp38-win32.whl", hash = "sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e"},
{file = "greenlet-0.4.15-cp38-cp38-win_amd64.whl", hash = "sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122"},
{file = "greenlet-0.4.15.tar.gz", hash = "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc"},
]
idna = [
{file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"},
{file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"},
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
]
imagesize = [
{file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
@ -689,10 +856,13 @@ itsdangerous = [
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
]
jinja2 = [
{file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"},
{file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"},
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
{file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
]
labthings = [
{file = "labthings-0.2.0-py3-none-any.whl", hash = "sha256:e2235c0db45c7134403bd78b2aa55ae5aa1bfee677735c9674f52bfcb605e998"},
{file = "labthings-0.2.0.tar.gz", hash = "sha256:f77391bbb02676ac7c28de59c425c4dc8d6de63031f2a2db8bffd26bc691cb5e"},
]
labthings = []
lazy-object-proxy = [
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
@ -747,39 +917,39 @@ markupsafe = [
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
marshmallow = [
{file = "marshmallow-3.3.0-py2.py3-none-any.whl", hash = "sha256:3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"},
{file = "marshmallow-3.3.0.tar.gz", hash = "sha256:0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e"},
{file = "marshmallow-3.5.1-py2.py3-none-any.whl", hash = "sha256:ac2e13b30165501b7d41fc0371b8df35944f5849769d136f20e2c5f6cdc6e665"},
{file = "marshmallow-3.5.1.tar.gz", hash = "sha256:90854221bbb1498d003a0c3cc9d8390259137551917961c8b5258c64026b2f85"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
numpy = [
{file = "numpy-1.18.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc"},
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121"},
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e"},
{file = "numpy-1.18.1-cp35-cp35m-win32.whl", hash = "sha256:f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"},
{file = "numpy-1.18.1-cp35-cp35m-win_amd64.whl", hash = "sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6"},
{file = "numpy-1.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480"},
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572"},
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57"},
{file = "numpy-1.18.1-cp36-cp36m-win32.whl", hash = "sha256:2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc"},
{file = "numpy-1.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd"},
{file = "numpy-1.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa"},
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca"},
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec"},
{file = "numpy-1.18.1-cp37-cp37m-win32.whl", hash = "sha256:d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73"},
{file = "numpy-1.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971"},
{file = "numpy-1.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07"},
{file = "numpy-1.18.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26"},
{file = "numpy-1.18.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474"},
{file = "numpy-1.18.1-cp38-cp38-win32.whl", hash = "sha256:590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3"},
{file = "numpy-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a"},
{file = "numpy-1.18.1.zip", hash = "sha256:b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77"},
{file = "numpy-1.18.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a1baa1dc8ecd88fb2d2a651671a84b9938461e8a8eed13e2f0a812a94084d1fa"},
{file = "numpy-1.18.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a244f7af80dacf21054386539699ce29bcc64796ed9850c99a34b41305630286"},
{file = "numpy-1.18.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6fcc5a3990e269f86d388f165a089259893851437b904f422d301cdce4ff25c8"},
{file = "numpy-1.18.2-cp35-cp35m-win32.whl", hash = "sha256:b5ad0adb51b2dee7d0ee75a69e9871e2ddfb061c73ea8bc439376298141f77f5"},
{file = "numpy-1.18.2-cp35-cp35m-win_amd64.whl", hash = "sha256:87902e5c03355335fc5992a74ba0247a70d937f326d852fc613b7f53516c0963"},
{file = "numpy-1.18.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9ab21d1cb156a620d3999dd92f7d1c86824c622873841d6b080ca5495fa10fef"},
{file = "numpy-1.18.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cdb3a70285e8220875e4d2bc394e49b4988bdb1298ffa4e0bd81b2f613be397c"},
{file = "numpy-1.18.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6d205249a0293e62bbb3898c4c2e1ff8a22f98375a34775a259a0523111a8f6c"},
{file = "numpy-1.18.2-cp36-cp36m-win32.whl", hash = "sha256:a35af656a7ba1d3decdd4fae5322b87277de8ac98b7d9da657d9e212ece76a61"},
{file = "numpy-1.18.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1598a6de323508cfeed6b7cd6c4efb43324f4692e20d1f76e1feec7f59013448"},
{file = "numpy-1.18.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:deb529c40c3f1e38d53d5ae6cd077c21f1d49e13afc7936f7f868455e16b64a0"},
{file = "numpy-1.18.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd77d58fb2acf57c1d1ee2835567cd70e6f1835e32090538f17f8a3a99e5e34b"},
{file = "numpy-1.18.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b1fe1a6f3a6f355f6c29789b5927f8bd4f134a4bd9a781099a7c4f66af8850f5"},
{file = "numpy-1.18.2-cp37-cp37m-win32.whl", hash = "sha256:2e40be731ad618cb4974d5ba60d373cdf4f1b8dcbf1dcf4d9dff5e212baf69c5"},
{file = "numpy-1.18.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4ba59db1fcc27ea31368af524dcf874d9277f21fd2e1f7f1e2e0c75ee61419ed"},
{file = "numpy-1.18.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59ca9c6592da581a03d42cc4e270732552243dc45e87248aa8d636d53812f6a5"},
{file = "numpy-1.18.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1b0ece94018ae21163d1f651b527156e1f03943b986188dd81bc7e066eae9d1c"},
{file = "numpy-1.18.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:82847f2765835c8e5308f136bc34018d09b49037ec23ecc42b246424c767056b"},
{file = "numpy-1.18.2-cp38-cp38-win32.whl", hash = "sha256:5e0feb76849ca3e83dd396254e47c7dba65b3fa9ed3df67c2556293ae3e16de3"},
{file = "numpy-1.18.2-cp38-cp38-win_amd64.whl", hash = "sha256:ba3c7a2814ec8a176bb71f91478293d633c08582119e713a0c5351c0f77698da"},
{file = "numpy-1.18.2.zip", hash = "sha256:e7894793e6e8540dbeac77c87b489e331947813511108ae097f1715c018b8f3d"},
]
packaging = [
{file = "packaging-20.0-py2.py3-none-any.whl", hash = "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb"},
{file = "packaging-20.0.tar.gz", hash = "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"},
{file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
{file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
]
picamera = []
pillow = [
@ -835,9 +1005,26 @@ pillow = [
{file = "Pillow-5.4.1.win32-py3.6.exe", hash = "sha256:4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d"},
{file = "Pillow-5.4.1.win32-py3.7.exe", hash = "sha256:75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522"},
]
psutil = [
{file = "psutil-5.7.0-cp27-none-win32.whl", hash = "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953"},
{file = "psutil-5.7.0-cp27-none-win_amd64.whl", hash = "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38"},
{file = "psutil-5.7.0-cp35-cp35m-win32.whl", hash = "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310"},
{file = "psutil-5.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5"},
{file = "psutil-5.7.0-cp36-cp36m-win32.whl", hash = "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e"},
{file = "psutil-5.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058"},
{file = "psutil-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8"},
{file = "psutil-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f"},
{file = "psutil-5.7.0-cp38-cp38-win32.whl", hash = "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4"},
{file = "psutil-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26"},
{file = "psutil-5.7.0.tar.gz", hash = "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pygments = [
{file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"},
{file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"},
{file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
{file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
]
pylint = [
{file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"},
@ -851,26 +1038,17 @@ pyserial = [
{file = "pyserial-3.4-py2.py3-none-any.whl", hash = "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"},
{file = "pyserial-3.4.tar.gz", hash = "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627"},
]
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
]
pytz = [
{file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
{file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
]
pyyaml = [
{file = "PyYAML-3.13-cp27-cp27m-win32.whl", hash = "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f"},
{file = "PyYAML-3.13-cp27-cp27m-win_amd64.whl", hash = "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537"},
{file = "PyYAML-3.13-cp34-cp34m-win32.whl", hash = "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3"},
{file = "PyYAML-3.13-cp34-cp34m-win_amd64.whl", hash = "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04"},
{file = "PyYAML-3.13-cp35-cp35m-win32.whl", hash = "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1"},
{file = "PyYAML-3.13-cp35-cp35m-win_amd64.whl", hash = "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613"},
{file = "PyYAML-3.13-cp36-cp36m-win32.whl", hash = "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a"},
{file = "PyYAML-3.13-cp36-cp36m-win_amd64.whl", hash = "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b"},
{file = "PyYAML-3.13-cp37-cp37m-win32.whl", hash = "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"},
{file = "PyYAML-3.13-cp37-cp37m-win_amd64.whl", hash = "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1"},
{file = "PyYAML-3.13.tar.gz", hash = "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf"},
]
requests = [
{file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"},
{file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"},
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
]
rope = [
{file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"},
@ -898,6 +1076,36 @@ scipy = [
{file = "scipy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e"},
{file = "scipy-1.3.0.tar.gz", hash = "sha256:c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a"},
]
simplejson = [
{file = "simplejson-3.17.0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:87d349517b572964350cc1adc5a31b493bbcee284505e81637d0174b2758ba17"},
{file = "simplejson-3.17.0-cp27-cp27m-win32.whl", hash = "sha256:1d1e929cdd15151f3c0b2efe953b3281b2fd5ad5f234f77aca725f28486466f6"},
{file = "simplejson-3.17.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1ea59f570b9d4916ae5540a9181f9c978e16863383738b69a70363bc5e63c4cb"},
{file = "simplejson-3.17.0-cp33-cp33m-win32.whl", hash = "sha256:8027bd5f1e633eb61b8239994e6fc3aba0346e76294beac22a892eb8faa92ba1"},
{file = "simplejson-3.17.0-cp33-cp33m-win_amd64.whl", hash = "sha256:22a7acb81968a7c64eba7526af2cf566e7e2ded1cb5c83f0906b17ff1540f866"},
{file = "simplejson-3.17.0-cp34-cp34m-win32.whl", hash = "sha256:17163e643dbf125bb552de17c826b0161c68c970335d270e174363d19e7ea882"},
{file = "simplejson-3.17.0-cp34-cp34m-win_amd64.whl", hash = "sha256:0fe3994207485efb63d8f10a833ff31236ed27e3b23dadd0bf51c9900313f8f2"},
{file = "simplejson-3.17.0-cp35-cp35m-win32.whl", hash = "sha256:4cf91aab51b02b3327c9d51897960c554f00891f9b31abd8a2f50fd4a0071ce8"},
{file = "simplejson-3.17.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fc9051d249dd5512e541f20330a74592f7a65b2d62e18122ca89bf71f94db748"},
{file = "simplejson-3.17.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86afc5b5cbd42d706efd33f280fec7bd7e2772ef54e3f34cf6b30777cd19a614"},
{file = "simplejson-3.17.0-cp36-cp36m-win32.whl", hash = "sha256:926bcbef9eb60e798eabda9cd0bbcb0fca70d2779aa0aa56845749d973eb7ad5"},
{file = "simplejson-3.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:daaf4d11db982791be74b23ff4729af2c7da79316de0bebf880fa2d60bcc8c5a"},
{file = "simplejson-3.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a126c3a91df5b1403e965ba63b304a50b53d8efc908a8c71545ed72535374a3"},
{file = "simplejson-3.17.0-cp37-cp37m-win32.whl", hash = "sha256:fc046afda0ed8f5295212068266c92991ab1f4a50c6a7144b69364bdee4a0159"},
{file = "simplejson-3.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7cce4bac7e0d66f3a080b80212c2238e063211fe327f98d764c6acbc214497fc"},
{file = "simplejson-3.17.0.tar.gz", hash = "sha256:2b4b2b738b3b99819a17feaf118265d0753d5536049ea570b3c43b51c4701e81"},
{file = "simplejson-3.17.0.win-amd64-py2.7.exe", hash = "sha256:1d346c2c1d7dd79c118f0cc7ec5a1c4127e0c8ffc83e7b13fc5709ff78c9bb84"},
{file = "simplejson-3.17.0.win-amd64-py3.3.exe", hash = "sha256:5cfd495527f8b85ce21db806567de52d98f5078a8e9427b18e251c68bd573a26"},
{file = "simplejson-3.17.0.win-amd64-py3.4.exe", hash = "sha256:8de378d589eccbc75941e480b4d5b4db66f22e4232f87543b136b1f093fff342"},
{file = "simplejson-3.17.0.win-amd64-py3.5.exe", hash = "sha256:f4b64a1031acf33e281fd9052336d6dad4d35eee3404c95431c8c6bc7a9c0588"},
{file = "simplejson-3.17.0.win-amd64-py3.6.exe", hash = "sha256:ad8dd3454d0c65c0f92945ac86f7b9efb67fa2040ba1b0189540e984df904378"},
{file = "simplejson-3.17.0.win-amd64-py3.7.exe", hash = "sha256:229edb079d5dd81bf12da952d4d825bd68d1241381b37d3acf961b384c9934de"},
{file = "simplejson-3.17.0.win32-py2.7.exe", hash = "sha256:4fd5f79590694ebff8dc980708e1c182d41ce1fda599a12189f0ca96bf41ad70"},
{file = "simplejson-3.17.0.win32-py3.3.exe", hash = "sha256:d140e9376e7f73c1f9e0a8e3836caf5eec57bbafd99259d56979da05a6356388"},
{file = "simplejson-3.17.0.win32-py3.4.exe", hash = "sha256:da00675e5e483ead345429d4f1374ab8b949fba4429d60e71ee9d030ced64037"},
{file = "simplejson-3.17.0.win32-py3.5.exe", hash = "sha256:7739940d68b200877a15a5ff5149e1599737d6dd55e302625650629350466418"},
{file = "simplejson-3.17.0.win32-py3.6.exe", hash = "sha256:60aad424e47c5803276e332b2a861ed7a0d46560e8af53790c4c4fb3420c26c2"},
{file = "simplejson-3.17.0.win32-py3.7.exe", hash = "sha256:1fbba86098bbfc1f85c5b69dc9a6d009055104354e0d9880bb00b692e30e0078"},
]
six = [
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
{file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
@ -907,20 +1115,20 @@ snowballstemmer = [
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
]
sphinx = [
{file = "Sphinx-2.3.1-py3-none-any.whl", hash = "sha256:298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159"},
{file = "Sphinx-2.3.1.tar.gz", hash = "sha256:e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5"},
{file = "Sphinx-2.4.4-py3-none-any.whl", hash = "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"},
{file = "Sphinx-2.4.4.tar.gz", hash = "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66"},
]
sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.1.tar.gz", hash = "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897"},
{file = "sphinxcontrib_applehelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"},
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
]
sphinxcontrib-devhelp = [
{file = "sphinxcontrib-devhelp-1.0.1.tar.gz", hash = "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34"},
{file = "sphinxcontrib_devhelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"},
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
]
sphinxcontrib-htmlhelp = [
{file = "sphinxcontrib-htmlhelp-1.0.2.tar.gz", hash = "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422"},
{file = "sphinxcontrib_htmlhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"},
{file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"},
{file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"},
]
sphinxcontrib-httpdomain = [
{file = "sphinxcontrib-httpdomain-1.7.0.tar.gz", hash = "sha256:ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"},
@ -931,12 +1139,12 @@ sphinxcontrib-jsmath = [
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
]
sphinxcontrib-qthelp = [
{file = "sphinxcontrib-qthelp-1.0.2.tar.gz", hash = "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"},
{file = "sphinxcontrib_qthelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20"},
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
]
sphinxcontrib-serializinghtml = [
{file = "sphinxcontrib-serializinghtml-1.1.3.tar.gz", hash = "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227"},
{file = "sphinxcontrib_serializinghtml-1.1.3-py2.py3-none-any.whl", hash = "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"},
{file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
{file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
]
toml = [
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
@ -967,17 +1175,17 @@ typed-ast = [
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
urllib3 = [
{file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"},
{file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"},
{file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"},
{file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"},
]
webargs = [
{file = "webargs-5.5.2-py2-none-any.whl", hash = "sha256:3f9dc15de183d356c9a0acc159c100ea0506c0c240c1e6f1d8b308c5fed4dbbd"},
{file = "webargs-5.5.2-py3-none-any.whl", hash = "sha256:fa4ad3ad9b38bedd26c619264fdc50d7ae014b49186736bca851e5b5228f2a1b"},
{file = "webargs-5.5.2.tar.gz", hash = "sha256:3beca296598067cec24a0b6f91c0afcc19b6e3c4d84ab026b931669628bb47b4"},
{file = "webargs-5.5.3-py2-none-any.whl", hash = "sha256:fc81c9f9d391acfbce406a319217319fd8b2fd862f7fdb5319ad06944f36ed25"},
{file = "webargs-5.5.3-py3-none-any.whl", hash = "sha256:4f04918864c7602886335d8099f9b8960ee698b6b914f022736ed50be6b71235"},
{file = "webargs-5.5.3.tar.gz", hash = "sha256:871642a2e0c62f21d5b78f357750ac7a87e6bc734c972f633aa5fb6204fbf29a"},
]
werkzeug = [
{file = "Werkzeug-0.16.0-py2.py3-none-any.whl", hash = "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"},
{file = "Werkzeug-0.16.0.tar.gz", hash = "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7"},
{file = "Werkzeug-1.0.0-py2.py3-none-any.whl", hash = "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16"},
{file = "Werkzeug-1.0.0.tar.gz", hash = "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096"},
]
wrapt = [
{file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"},

View file

@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry]
name = "openflexure_microscope"
version = "2.0.0-beta.2"
version = "2.0.0"
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
authors = [
@ -25,16 +25,17 @@ license = "GPL-3.0"
[tool.poetry.dependencies]
python = "^3.6"
Flask = "^1.0"
pyyaml = "^3.13"
numpy = "^1.17.0"
Pillow = "^5.4"
scipy = "1.3.0" # Exact version until we can guarantee a wheel
picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch.
picamera = { git = "https://github.com/rwb27/picamera.git", branch = "master", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch.
"RPi.GPIO" = { version = "^0.6.5", optional = true }
pyserial = "^3.4" # Used for sangaboard (basic_serial_instrument) until we move to sangaboard pip library
labthings = { git = "https://github.com/labthings/python-labthings.git", branch = "master"} # TODO: Pin to a fixed version before 2.0.0 stable
python-dateutil = "^2.8"
psutil = "^5.6.7" # Autostorage extension
labthings = "0.2.0"
[tool.poetry.extras]
rpi = ["picamera", "RPi.GPIO"]