Merge branch 'new-semantics-testing' into 'master'

New metadata semantics

See merge request openflexure/openflexure-microscope-server!38
This commit is contained in:
Joel Collins 2020-01-29 17:48:58 +00:00
commit ae589a7063
28 changed files with 536 additions and 861 deletions

View file

@ -14,7 +14,7 @@ from labthings.server import fields
class MicroscopeIdentifySchema(Schema): class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID 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) camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage 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): class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID 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) camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage 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): class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID 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) camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)

View file

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

View file

@ -19,7 +19,7 @@ from openflexure_microscope.paths import (
OPENFLEXURE_VAR_PATH, OPENFLEXURE_VAR_PATH,
OPENFLEXURE_EXTENSIONS_PATH, OPENFLEXURE_EXTENSIONS_PATH,
settings_file_path, settings_file_path,
logs_file_path logs_file_path,
) )
from labthings.server.quick import create_app from labthings.server.quick import create_app
@ -92,15 +92,21 @@ labthing.add_root_link(views.CaptureList, "captures")
labthing.add_view(views.CaptureView, f"/captures/<id>") labthing.add_view(views.CaptureView, f"/captures/<id>")
labthing.add_view(views.CaptureDownload, f"/captures/<id>/download/<filename>") labthing.add_view(views.CaptureDownload, f"/captures/<id>/download/<filename>")
labthing.add_view(views.CaptureTags, f"/captures/<id>/tags") 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 # Attach streams resources
labthing.add_view(views.MjpegStream, f"/streams/mjpeg") labthing.add_view(views.MjpegStream, f"/streams/mjpeg")

View file

@ -21,8 +21,8 @@ def recalibrate(microscope):
""" """
scamera = microscope.camera scamera = microscope.camera
with scamera.lock: with scamera.lock:
assert not scamera.status["record_active"], "Can't recalibrate while recording!" assert not scamera.record_active, "Can't recalibrate while recording!"
streaming = scamera.status["stream_active"] streaming = scamera.stream_active
if streaming: if streaming:
logging.info("Stopping stream before recalibration") logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480)) scamera.stop_stream_recording(resolution=(640, 480))

View file

@ -1,6 +1,7 @@
import itertools import itertools
import logging import logging
import uuid import uuid
import datetime
from typing import Tuple from typing import Tuple
from functools import reduce from functools import reduce
@ -71,12 +72,12 @@ def progress():
def capture( def capture(
microscope, microscope,
basename, basename,
scan_id,
temporary: bool = False, temporary: bool = False,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
@ -94,16 +95,14 @@ def capture(
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer 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 # Inject system metadata
output.put_metadata(microscope.metadata, system=True) output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata # Insert custom metadata
output.put_metadata(metadata) output.put_metadata(metadata)
# Insert custom metadata
output.put_annotations(annotations)
# Insert custom tags # Insert custom tags
output.put_tags(tags) output.put_tags(tags)
@ -115,7 +114,7 @@ def tile(
microscope, microscope,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
step_size: int = [2000, 1500, 100], stride_size: int = [2000, 1500, 100],
grid: list = [3, 3, 5], grid: list = [3, 3, 5],
style="raster", style="raster",
autofocus_dz: int = 50, autofocus_dz: int = 50,
@ -124,13 +123,13 @@ def tile(
bayer: bool = False, bayer: bool = False,
fast_autofocus=False, fast_autofocus=False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
global _images_to_be_captured global _images_to_be_captured
global _images_captured_so_far global _images_captured_so_far
# Keep task progress # Keep task progress
# TODO: Make this line not nasty
_images_to_be_captured = reduce((lambda x, y: x * y), grid) _images_to_be_captured = reduce((lambda x, y: x * y), grid)
_images_captured_so_far = 0 _images_captured_so_far = 0
@ -138,28 +137,22 @@ def tile(
if not basename: if not basename:
basename = generate_basename() basename = generate_basename()
# Generate a stack ID
scan_id = uuid.uuid4()
# Store initial position # Store initial position
initial_position = microscope.stage.position initial_position = microscope.stage.position
# Add scan metadata # Add dataset metadata
if "time" not in metadata: dataset_d = {
metadata["time"] = generate_basename() "dataset": {
"id": uuid.uuid4(),
metadata.update( "type": "xyzScan",
{ "name": basename,
"scan_id": scan_id, "acquisitionDate": datetime.datetime.now().isoformat(),
"basename": basename, "strideSize": stride_size,
"scan_parameters": { "grid": grid,
"step_size": step_size, "style": style,
"grid": grid, "autofocusDz": autofocus_dz,
"style": style,
"autofocus_dz": autofocus_dz,
},
} }
) }
# Check if autofocus is enabled # Check if autofocus is enabled
autofocus_extension = find_extension("org.openflexure.autofocus") autofocus_extension = find_extension("org.openflexure.autofocus")
@ -174,11 +167,11 @@ def tile(
autofocus_enabled = False autofocus_enabled = False
z_stack_dz = ( z_stack_dz = (
grid[2] * step_size[2] if grid[2] > 1 else 0 grid[2] * stride_size[2] if grid[2] > 1 else 0
) # shorthand for Z stack range ) # shorthand for Z stack range
# Construct an x-y grid (worry about z later) # 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 # Keep the initial Z position the same as our current position
next_z = initial_position[2] next_z = initial_position[2]
@ -209,26 +202,25 @@ def tile(
target_z=-z_stack_dz / 2.0, # Finish below the focus target_z=-z_stack_dz / 2.0, # Finish below the focus
initial_move_up=False, # We're already at the top of the scan initial_move_up=False, # We're already at the top of the scan
) )
# TODO: save the focus data for future reference? Use it for diagnostics?
else: else:
logging.debug("Running autofocus") logging.debug("Running autofocus")
autofocus_extension.autofocus( autofocus_extension.autofocus(
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz) range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
) )
logging.debug("Finished autofocus") logging.debug("Finished autofocus")
time.sleep(1) # TODO: Remove time.sleep(1)
# If we're not doing a z-stack, just capture # If we're not doing a z-stack, just capture
if grid[2] <= 1: if grid[2] <= 1:
capture( capture(
microscope, microscope,
basename, basename,
scan_id,
temporary=temporary, temporary=temporary,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=dataset_d,
annotations=annotations,
tags=tags, tags=tags,
) )
# Update task progress # Update task progress
@ -240,15 +232,14 @@ def tile(
microscope=microscope, microscope=microscope,
basename=basename, basename=basename,
temporary=temporary, temporary=temporary,
scan_id=scan_id, step_size=stride_size[2],
step_size=step_size[2],
steps=grid[2], steps=grid[2],
center=not fast_autofocus, # fast_autofocus does this for us!
return_to_start=not fast_autofocus, return_to_start=not fast_autofocus,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=dataset_d,
annotations=annotations,
tags=tags, tags=tags,
) )
# Make sure we use our current best estimate of focus (i.e. the current position) next point # Make sure we use our current best estimate of focus (i.e. the current position) next point
@ -259,7 +250,7 @@ def tile(
) # Fast autofocus requires us to start at the top of the range ) # Fast autofocus requires us to start at the top of the range
if grid[2] > 1: if grid[2] > 1:
next_z -= int( next_z -= int(
grid[2] / 2.0 * step_size[2] grid[2] / 2.0 * stride_size[2]
) # Z stacking means we're higher up to start with ) # Z stacking means we're higher up to start with
logging.debug("Returning to {}".format(initial_position)) logging.debug("Returning to {}".format(initial_position))
@ -270,39 +261,25 @@ def stack(
microscope, microscope,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
scan_id: str = None,
step_size: int = 100, step_size: int = 100,
steps: int = 5, steps: int = 5,
center: bool = True,
return_to_start: bool = True, return_to_start: bool = True,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
global _images_captured_so_far 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 # Store initial position
initial_position = microscope.stage.position initial_position = microscope.stage.position
with microscope.lock: with microscope.lock:
# Move to center scan # Move to center scan
if center: logging.debug("Moving to starting position")
logging.debug("Moving to starting position") microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
for i in range(steps): for i in range(steps):
time.sleep(0.1) time.sleep(0.1)
@ -310,12 +287,12 @@ def stack(
capture( capture(
microscope, microscope,
basename, basename,
scan_id,
temporary=temporary, temporary=temporary,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=metadata,
annotations=annotations,
tags=tags, tags=tags,
) )
# Update task progress # Update task progress
@ -337,16 +314,18 @@ def stack(
class TileScanAPI(View): class TileScanAPI(View):
@use_args( @use_args(
{ {
"filename": fields.String(), "filename": fields.String(missing=None, example=None),
"temporary": fields.Boolean(missing=False), "temporary": fields.Boolean(missing=False),
"step_size": fields.List(fields.Integer, missing=[2000, 1500, 100]), "stride_size": fields.List(
"grid": fields.List(fields.Integer, missing=[3, 3, 5]), 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"), "style": fields.String(missing="raster"),
"autofocus_dz": fields.Integer(missing=50), "autofocus_dz": fields.Integer(missing=50),
"fast_autofocus": fields.Boolean(missing=False), "fast_autofocus": fields.Boolean(missing=False),
"use_video_port": fields.Boolean(missing=False), "use_video_port": fields.Boolean(missing=False),
"bayer": 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=[]), "tags": fields.List(fields.String, missing=[]),
"resize": fields.Dict(missing=None), # TODO: Validate keys "resize": fields.Dict(missing=None), # TODO: Validate keys
} }
@ -373,7 +352,7 @@ class TileScanAPI(View):
microscope, microscope,
basename=args.get("filename"), basename=args.get("filename"),
temporary=args.get("temporary"), temporary=args.get("temporary"),
step_size=args.get("step_size"), stride_size=args.get("stride_size"),
grid=args.get("grid"), grid=args.get("grid"),
style=args.get("style"), style=args.get("style"),
autofocus_dz=args.get("autofocus_dz"), autofocus_dz=args.get("autofocus_dz"),
@ -381,7 +360,7 @@ class TileScanAPI(View):
resize=resize, resize=resize,
bayer=args.get("bayer"), bayer=args.get("bayer"),
fast_autofocus=args.get("fast_autofocus"), fast_autofocus=args.get("fast_autofocus"),
metadata=args.get("metadata"), annotations=args.get("annotations"),
tags=args.get("tags"), tags=args.get("tags"),
) )

View file

@ -73,17 +73,15 @@ class ZipManager:
# Update task progress # Update task progress
update_task_progress(int((index / n_files) * 100)) update_task_progress(int((index / n_files) * 100))
session_id = uuid.uuid4() session_id = str(uuid.uuid4())
session_key = str(session_id) self.session_zips[session_id] = {
# self.session_zips[session_id] = fp
self.session_zips[session_key] = {
"id": session_id, "id": session_id,
"fp": fp, "fp": fp,
"data_size": data_size_megabytes, "data_size": data_size_megabytes,
"zip_size": os.path.getsize(fp.name) * 1e-6, "zip_size": os.path.getsize(fp.name) * 1e-6,
} }
return self.session_zips[session_key] return self.session_zips[session_id]
def zip_from_id(self, session_id): def zip_from_id(self, session_id):
return self.session_zips[session_id]["fp"] return self.session_zips[session_id]["fp"]
@ -103,13 +101,13 @@ class ZipBuilderAPIView(View):
task = taskify(default_zip_manager.build_zip_from_capture_ids)(microscope, ids) task = taskify(default_zip_manager.build_zip_from_capture_ids)(microscope, ids)
# Return a handle on the autofocus task # Return a handle on the autofocus task
return jsonify(task.state), 201 return task.state, 201
@ThingProperty @ThingProperty
class ZipListAPIView(View): class ZipListAPIView(View):
def get(self): def get(self):
return jsonify(default_zip_manager.session_zips) return default_zip_manager.session_zips
class ZipGetterAPIView(View): class ZipGetterAPIView(View):
@ -141,7 +139,7 @@ class ZipGetterAPIView(View):
del default_zip_manager.session_zips[session_id] 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-beta.1")

View file

@ -3,38 +3,9 @@ from openflexure_microscope.camera.capture import build_captures_from_exif
import logging 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() 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 # Restore loaded capture array to camera object
logging.debug("Restoring captures...") logging.debug("Restoring captures...")
default_microscope.camera.images = build_captures_from_exif( default_microscope.camera.images = build_captures_from_exif(

View file

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

View file

@ -36,7 +36,7 @@ class CaptureAPI(View):
"bayer": fields.Boolean( "bayer": fields.Boolean(
missing=False, description="Store raw bayer data in file" 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"]), "tags": fields.List(fields.String, missing=[], example=["docs"]),
"resize": fields.Dict( "resize": fields.Dict(
missing=None, example={"width": 640, "height": 480} missing=None, example={"width": 640, "height": 480}
@ -75,11 +75,10 @@ class CaptureAPI(View):
) )
# Inject system metadata # Inject system metadata
output.put_metadata(microscope.metadata, system=True) output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata # Insert custom metadata
output.put_metadata(args.get("metadata")) output.put_annotations(args.get("annotations"))
# Insert custom tags # Insert custom tags
output.put_tags(args.get("tags")) output.put_tags(args.get("tags"))
@ -164,8 +163,8 @@ class GPUPreviewStartAPI(View):
microscope.camera.start_preview(fullscreen=fullscreen, window=window) microscope.camera.start_preview(fullscreen=fullscreen, window=window)
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status) return jsonify(microscope.state)
@ThingAction @ThingAction
@ -176,5 +175,5 @@ class GPUPreviewStopAPI(View):
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
microscope.camera.stop_preview() microscope.camera.stop_preview()
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status) return jsonify(microscope.state)

View file

@ -52,8 +52,8 @@ class MoveStageAPI(View):
else: else:
logging.warning("Unable to move. No stage found.") logging.warning("Unable to move. No stage found.")
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status["stage"]["position"]) return jsonify(microscope.state["stage"]["position"])
@ThingAction @ThingAction
@ -66,5 +66,5 @@ class ZeroStageAPI(View):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
microscope.stage.zero_position() microscope.stage.zero_position()
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status["stage"]) return jsonify(microscope.state["stage"])

View file

@ -20,7 +20,7 @@ class CaptureSchema(Schema):
data_key="path", description="Path of file on microscope device" data_key="path", description="Path of file on microscope device"
) )
exists = fields.Bool(data_key="available") exists = fields.Bool(data_key="available")
filename = fields.String() name = fields.String()
metadata = fields.Dict() metadata = fields.Dict()
links = fields.Dict() links = fields.Dict()
@ -39,16 +39,18 @@ class CaptureSchema(Schema):
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureTags), **description_from_view(CaptureTags),
}, },
"metadata": { "annotations": {
"href": url_for(CaptureMetadata.endpoint, id=data.id, _external=True), "href": url_for(
CaptureAnnotations.endpoint, id=data.id, _external=True
),
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureMetadata), **description_from_view(CaptureAnnotations),
}, },
"download": { "download": {
"href": url_for( "href": url_for(
CaptureDownload.endpoint, CaptureDownload.endpoint,
id=data.id, id=data.id,
filename=data.filename, filename=data.name,
_external=True, _external=True,
), ),
"mimetype": "image/jpeg", "mimetype": "image/jpeg",
@ -62,6 +64,9 @@ capture_schema = CaptureSchema()
capture_list_schema = CaptureSchema(many=True) capture_list_schema = CaptureSchema(many=True)
from pprint import pprint
@ThingProperty @ThingProperty
@Tag("captures") @Tag("captures")
class CaptureList(View): class CaptureList(View):
@ -197,10 +202,10 @@ class CaptureTags(View):
@Tag("captures") @Tag("captures")
class CaptureMetadata(View): class CaptureAnnotations(View):
def get(self, id): 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") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.image_from_id(id)
@ -208,7 +213,7 @@ class CaptureMetadata(View):
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
return jsonify(capture_obj.metadata) return jsonify(capture_obj.annotations)
def put(self, id): def put(self, id):
""" """
@ -226,7 +231,6 @@ class CaptureMetadata(View):
if type(data_dict) != dict: if type(data_dict) != dict:
return abort(400) return abort(400)
# TODO: Allow putting system metadata maybe? capture_obj.put_annotations(data_dict)
capture_obj.put_metadata(data_dict)
return jsonify(capture_obj.metadata) return jsonify(capture_obj.annotations)

View file

@ -30,7 +30,7 @@ class SettingsProperty(View):
logging.debug("Updating settings from PUT request:") logging.debug("Updating settings from PUT request:")
logging.debug(payload.json) logging.debug(payload.json)
microscope.apply_settings(payload.json) microscope.update_settings(payload.json)
microscope.save_settings() microscope.save_settings()
return self.get() return self.get()
@ -65,24 +65,24 @@ class NestedSettingsProperty(View):
dictionary = create_from_path(keys) dictionary = create_from_path(keys)
set_by_path(dictionary, keys, payload.json) set_by_path(dictionary, keys, payload.json)
microscope.apply_settings(dictionary) microscope.update_settings(dictionary)
microscope.save_settings() microscope.save_settings()
return self.get(route) return self.get(route)
@ThingProperty @ThingProperty
class StatusProperty(View): class StateProperty(View):
def get(self): def get(self):
""" """
Show current read-only state of the microscope Show current read-only state of the microscope
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
return jsonify(microscope.status) return jsonify(microscope.state)
@Tag("properties") @Tag("properties")
class NestedStatusProperty(View): class NestedStateProperty(View):
@doc_response(404, description="Status key cannot be found") @doc_response(404, description="Status key cannot be found")
def get(self, route): def get(self, route):
""" """
@ -92,7 +92,35 @@ class NestedStatusProperty(View):
keys = route.split("/") keys = route.split("/")
try: 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: except KeyError:
return abort(404) return abort(404)

View file

@ -91,20 +91,6 @@ class CameraEvent(object):
class BaseCamera(metaclass=ABCMeta): class BaseCamera(metaclass=ABCMeta):
""" """
Base implementation of StreamingCamera. 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): def __init__(self):
@ -121,7 +107,8 @@ class BaseCamera(metaclass=ABCMeta):
self.stream_timeout = 20 self.stream_timeout = 20
self.stream_timeout_enabled = False 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} self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
@ -129,8 +116,24 @@ class BaseCamera(metaclass=ABCMeta):
self.images = [] self.images = []
self.videos = [] self.videos = []
@property
@abstractmethod @abstractmethod
def apply_settings(self, config: dict): 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 update_settings(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
with self.lock: with self.lock:
# Apply valid config params to camera object # Apply valid config params to camera object
@ -187,7 +190,7 @@ class BaseCamera(metaclass=ABCMeta):
self.last_access = time.time() self.last_access = time.time()
self.stop = False self.stop = False
if not self.status["stream_active"]: if not self.stream_active:
# start background frame thread # start background frame thread
self.thread = threading.Thread(target=self._thread) self.thread = threading.Thread(target=self._thread)
self.thread.daemon = True self.thread.daemon = True
@ -207,12 +210,12 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("Stopping worker thread") logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
if self.status["stream_active"]: if self.stream_active:
self.stop = True self.stop = True
self.thread.join() # Wait for stream thread to exit self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting 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: if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.") logging.debug("Timeout waiting for worker thread close.")
raise TimeoutError("Timeout waiting for worker thread close.") raise TimeoutError("Timeout waiting for worker thread close.")
@ -352,7 +355,7 @@ class BaseCamera(metaclass=ABCMeta):
self.frames_iterator = self.frames() self.frames_iterator = self.frames()
logging.debug("Entering worker thread.") logging.debug("Entering worker thread.")
self.status["stream_active"] = True self.stream_active = True
for frame in self.frames_iterator: for frame in self.frames_iterator:
self.frame = frame self.frame = frame
@ -365,9 +368,7 @@ class BaseCamera(metaclass=ABCMeta):
and ( # If using timeout and ( # If using timeout
time.time() - self.last_access > self.stream_timeout time.time() - self.last_access > self.stream_timeout
) )
and not self.status[ # And timeout time and not self.preview_active # And GPU preview is not active
"preview_active"
] # And GPU preview is not active
): ):
self.frames_iterator.close() self.frames_iterator.close()
break break
@ -383,4 +384,4 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("BaseCamera worker thread exiting...") logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state # Set stream_activate state
self.status["stream_active"] = False self.stream_active = False

View file

@ -8,6 +8,7 @@ import yaml
import json import json
import logging import logging
from PIL import Image from PIL import Image
import dateutil.parser
import atexit import atexit
from openflexure_microscope.camera import piexif from openflexure_microscope.camera import piexif
@ -93,18 +94,18 @@ def capture_from_exif(path, exif_dict):
# Build file path information # Build file path information
capture.split_file_path(capture.file) capture.split_file_path(capture.file)
# Populate capture parameters # Image metadata
capture.id = exif_dict["id"] image_metadata = exif_dict.pop("image")
capture.timestring = exif_dict["time"]
capture.format = exif_dict["format"]
capture.custom_metadata = ( # Populate capture parameters
exif_dict["custom"] if "custom" in exif_dict.keys() else {} capture.id = image_metadata.get("id")
) capture.datetime = dateutil.parser.isoparse(image_metadata.get("acquisitionDate"))
capture.system_metadata = ( capture.format = image_metadata.get("format")
exif_dict["system"] if "system" in exif_dict.keys() else {} capture.tags = image_metadata.get("tags")
) capture.annotations = image_metadata.get("annotations")
capture.tags = exif_dict["tags"]
# Since we popped the "image" key, we dump whatever is left in _metadata
capture._metadata = exif_dict
return capture return capture
@ -113,16 +114,6 @@ class CaptureObject(object):
""" """
StreamObject used to store and process on-disk capture data, and metadata. StreamObject used to store and process on-disk capture data, and metadata.
Serves to simplify modifying properties of on-disk capture data. 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: def __init__(self, filepath) -> None:
@ -131,17 +122,20 @@ class CaptureObject(object):
# Store a nice ID # Store a nice ID
self.id = uuid.uuid4() #: str: Unique capture ID self.id = uuid.uuid4() #: str: Unique capture ID
logging.debug("Created StreamObject {}".format(self.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 # Create file name. Default to UUID
self.file = filepath self.file = filepath
self.split_file_path(self.file) self.split_file_path(self.file)
# Dictionary for storing custom metadata if not os.path.exists(self.filefolder):
self.custom_metadata = {} os.makedirs(self.filefolder)
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
self.system_metadata = {}
# 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 # List for storing tags
self.tags = [] self.tags = []
@ -158,15 +152,11 @@ class CaptureObject(object):
Args: Args:
filepath (str): String of the full file path, including file format extension filepath (str): String of the full file path, including file format extension
""" """
# Split the full file path into a folder and a filename # Split the full file path into a folder and a name
self.filefolder, self.filename = os.path.split(filepath) self.filefolder, self.name = os.path.split(filepath)
# Split the filename out from it's file extension # Split the name out from it's file extension
self.basename = os.path.splitext(self.filename)[0] self.basename = os.path.splitext(self.name)[0]
self.format = self.filename.split(".")[-1] self.format = self.name.split(".")[-1]
# Create folder and file
if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder)
@property @property
def exists(self) -> bool: def exists(self) -> bool:
@ -204,17 +194,24 @@ class CaptureObject(object):
# HANDLE METADATA # 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: Args:
data (dict): Dictionary of metadata to be added data (dict): Dictionary of metadata to be added
""" """
if system: self.annotations.update(data)
self.system_metadata.update(data) self.save_metadata()
else:
self.custom_metadata.update(data) 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() self.save_metadata()
def save_metadata(self) -> None: def save_metadata(self) -> None:
@ -244,12 +241,15 @@ class CaptureObject(object):
and any added custom metadata and tags. and any added custom metadata and tags.
""" """
d = { d = {
"id": self.id, "image": {
"time": self.timestring, "id": self.id,
"format": self.format, "name": self.name,
"tags": self.tags, "acquisitionDate": self.datetime.isoformat(),
"custom": self.custom_metadata, "format": self.format,
"system": self.system_metadata, "tags": self.tags,
"annotations": self.annotations,
},
**self._metadata,
} }
# Add custom metadata to dictionary # Add custom metadata to dictionary
@ -262,7 +262,7 @@ class CaptureObject(object):
""" """
# Create basic state dictionary # 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 # Combined availability of data
if self.exists: 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 = logging.getLogger("PIL")
pil_logger.setLevel(logging.INFO) pil_logger.setLevel(logging.INFO)
# MAIN CLASS # MAIN CLASS
class MockStreamer(BaseCamera): class MissingCamera(BaseCamera):
def __init__(self): def __init__(self):
# Run BaseCamera init # Run BaseCamera init
BaseCamera.__init__(self) BaseCamera.__init__(self)
# Store state of PiCameraStreamer
self.status.update(
{"stream_active": False, "record_active": False, "board": None}
)
# Update config properties # Update config properties
self.image_resolution = (1312, 976) self.image_resolution = (1312, 976)
self.stream_resolution = (640, 480) self.stream_resolution = (640, 480)
@ -71,6 +67,16 @@ class MockStreamer(BaseCamera):
image.save(self.stream, format="JPEG") 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): def initialisation(self):
"""Run any initialisation code when the frame iterator starts.""" """Run any initialisation code when the frame iterator starts."""
pass pass
@ -101,7 +107,7 @@ class MockStreamer(BaseCamera):
return conf_dict return conf_dict
def apply_settings(self, config: dict): def update_settings(self, config: dict):
""" """
Write a config dictionary to the PiCameraStreamer config. Write a config dictionary to the PiCameraStreamer config.
@ -117,7 +123,7 @@ class MockStreamer(BaseCamera):
with self.lock: with self.lock:
# Apply valid config params to camera object # 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 for key, value in config.items(): # For each provided setting
if hasattr(self, key): if hasattr(self, key):

View file

@ -47,6 +47,7 @@ from .base import BaseCamera, CaptureObject
from .set_picamera_gain import set_analog_gain, set_digital_gain from .set_picamera_gain import set_analog_gain, set_digital_gain
from openflexure_microscope.paths import settings_file_path from openflexure_microscope.paths import settings_file_path
from openflexure_microscope.utilities import serialise_array_b64
# MAIN CLASS # MAIN CLASS
@ -84,15 +85,8 @@ class PiCameraStreamer(BaseCamera):
picamera.PiCamera() picamera.PiCamera()
) #: :py:class:`picamera.PiCamera`: Picamera object ) #: :py:class:`picamera.PiCamera`: Picamera object
# Store status of PiCameraStreamer # Store state of PiCameraStreamer
self.status.update( self.preview_active = False
{
"stream_active": False,
"record_active": False,
"preview_active": False,
"board": f"picamera_{self.camera.revision}",
}
)
# Reset variable states # Reset variable states
self.set_zoom(1.0) self.set_zoom(1.0)
@ -116,15 +110,22 @@ class PiCameraStreamer(BaseCamera):
"picamera_lst.npy" "picamera_lst.npy"
) #: str: Path of .npy lens shading table file ) #: str: Path of .npy lens shading table file
# Update board identifier
self.status.update({})
# Create an empty stream # Create an empty stream
self.stream = io.BytesIO() self.stream = io.BytesIO()
# Start streaming # Start streaming
self.start_worker() 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): def initialisation(self):
"""Run any initialisation code when the frame iterator starts.""" """Run any initialisation code when the frame iterator starts."""
pass pass
@ -156,7 +157,7 @@ class PiCameraStreamer(BaseCamera):
"picamera_lst_path": self.picamera_lst_path "picamera_lst_path": self.picamera_lst_path
if os.path.isfile(self.picamera_lst_path) if os.path.isfile(self.picamera_lst_path)
else None, else None,
"picamera_settings": {}, "picamera": {},
} }
) )
@ -165,7 +166,7 @@ class PiCameraStreamer(BaseCamera):
try: try:
value = getattr(self.camera, key) value = getattr(self.camera, key)
logging.debug("Reading PiCamera().{}: {}".format(key, value)) logging.debug("Reading PiCamera().{}: {}".format(key, value))
conf_dict["picamera_settings"][key] = value conf_dict["picamera"][key] = value
except AttributeError: except AttributeError:
logging.debug("Unable to read PiCamera attribute {}".format(key)) logging.debug("Unable to read PiCamera attribute {}".format(key))
@ -176,7 +177,7 @@ class PiCameraStreamer(BaseCamera):
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path)) logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
self.save_lens_shading_table() 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. Write a config dictionary to the PiCameraStreamer config.
@ -194,23 +195,23 @@ class PiCameraStreamer(BaseCamera):
with self.lock: with self.lock:
# Apply valid config params to Picamera object # 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 # 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.") logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done paused_stream = True # Remember to unpause stream when done
# PiCamera parameters # 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( self.apply_picamera_settings(
config["picamera_settings"], pause_for_effect=True config["picamera"], pause_for_effect=True
) )
# PiCameraStreamer parameters # PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting 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) setattr(self, key, value)
# Handle lens shading if camera supports it # Handle lens shading if camera supports it
@ -332,13 +333,13 @@ class PiCameraStreamer(BaseCamera):
Change the camera zoom, handling re-centering and scaling. Change the camera zoom, handling re-centering and scaling.
""" """
with self.lock: with self.lock:
self.status["zoom_value"] = float(zoom_value) self.zoom_value = float(zoom_value)
if self.status["zoom_value"] < 1: if self.zoom_value < 1:
self.status["zoom_value"] = 1 self.zoom_value = 1
# Richard's code for zooming ! # Richard's code for zooming !
fov = self.camera.zoom fov = self.camera.zoom
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0]) 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 # If the new zoom value would be invalid, move the centre to
# keep it within the camera's sensor (this is only relevant # 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) # when zooming out, if the FoV is not centred on (0.5, 0.5)
@ -365,7 +366,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.preview.window = window self.camera.preview.window = window
if fullscreen: if fullscreen:
self.camera.preview.fullscreen = fullscreen self.camera.preview.fullscreen = fullscreen
self.status["preview_active"] = True self.preview_active = True
except picamera.exc.PiCameraMMALError as e: except picamera.exc.PiCameraMMALError as e:
logging.error( logging.error(
"Suppressed a MMALError in start_preview. Exception: {}".format(e) "Suppressed a MMALError in start_preview. Exception: {}".format(e)
@ -380,7 +381,7 @@ class PiCameraStreamer(BaseCamera):
def stop_preview(self): def stop_preview(self):
"""Stop the on board GPU camera preview.""" """Stop the on board GPU camera preview."""
self.camera.stop_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): def start_recording(self, output, fmt: str = "h264", quality: int = 15):
"""Start recording. """Start recording.
@ -398,7 +399,7 @@ class PiCameraStreamer(BaseCamera):
""" """
with self.lock: with self.lock:
# Start recording method only if a current recording is not running # 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 # Start the camera video recording on port 2
logging.info("Recording to {}".format(output)) logging.info("Recording to {}".format(output))
@ -411,8 +412,8 @@ class PiCameraStreamer(BaseCamera):
quality=quality, quality=quality,
) )
# Update status dictionary # Update state
self.status["record_active"] = True self.record_active = True
return output return output
@ -431,8 +432,8 @@ class PiCameraStreamer(BaseCamera):
self.camera.stop_recording(splitter_port=2) self.camera.stop_recording(splitter_port=2)
logging.info("Recording stopped") logging.info("Recording stopped")
# Update status dictionary # Update state
self.status["record_active"] = False self.record_active = False
def stop_stream_recording( def stop_stream_recording(
self, splitter_port: int = 1, resolution: Tuple[int, int] = None self, splitter_port: int = 1, resolution: Tuple[int, int] = None
@ -500,7 +501,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.resolution = resolution self.camera.resolution = resolution
# If the stream should be active # If the stream should be active
if self.status["stream_active"]: if self.stream_active:
try: try:
# Start recording on stream port # Start recording on stream port
self.camera.start_recording( self.camera.start_recording(
@ -551,7 +552,7 @@ class PiCameraStreamer(BaseCamera):
if isinstance(output, CaptureObject): if isinstance(output, CaptureObject):
target = output.file target = output.file
else: else:
target = target target = output
with self.lock: with self.lock:
logging.info("Capturing to {}".format(output)) logging.info("Capturing to {}".format(output))
@ -680,7 +681,6 @@ class PiCameraStreamer(BaseCamera):
# Start stream recording (and set resolution) # Start stream recording (and set resolution)
self.start_stream_recording() self.start_stream_recording()
# Update status
logging.debug("STREAM ACTIVE") logging.debug("STREAM ACTIVE")
# While the iterator is not closed # While the iterator is not closed

View file

@ -1,4 +1,5 @@
import json import json
import flask
import os import os
import errno import errno
import logging import logging
@ -7,7 +8,12 @@ from uuid import UUID
import numpy as np import numpy as np
from fractions import Fraction from fractions import Fraction
from .paths import 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: class OpenflexureSettingsFile:
@ -19,21 +25,21 @@ class OpenflexureSettingsFile:
expand (bool): Expand paths to valid auxillary config files. expand (bool): Expand paths to valid auxillary config files.
""" """
def __init__(self, config_path: str = None): def __init__(self, path: str, defaults: dict = {}):
global DEFAULT_CONFIG, CONFIG_FILE_PATH global DEFAULT_SETTINGS
# Set arguments # 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 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: def load(self) -> dict:
""" """
Loads settings from a file on-disk. Loads settings from a file on-disk.
""" """
# Unexpanded config dictionary (used at load/save time) # 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") logging.debug("Reading settings from disk")
return loaded_config return loaded_config
@ -50,11 +56,11 @@ class OpenflexureSettingsFile:
save_settings = config save_settings = config
if backup: if backup:
if os.path.isfile(self.config_path): if os.path.isfile(self.path):
shutil.copyfile(self.config_path, self.config_path + ".bk") shutil.copyfile(self.path, self.path + ".bk")
logging.debug("Saving settings dictionary to disk") 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: def merge(self, config: dict) -> dict:
""" """
@ -71,7 +77,7 @@ class OpenflexureSettingsFile:
return settings 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 A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
""" """
@ -88,11 +94,14 @@ class JSONEncoder(json.JSONEncoder):
# Numpy arrays # Numpy arrays
elif isinstance(o, np.ndarray): elif isinstance(o, np.ndarray):
return o.tolist() return o.tolist()
# UUIDs
elif isinstance(o, UUID):
return str(o)
else: else:
# call base class implementation which takes care of # call base class implementation which takes care of
# raising exceptions for unsupported types # raising exceptions for unsupported types
try: try:
return json.JSONEncoder.default(self, o) return flask.json.JSONEncoder.default(self, o)
# if it's some mystery object, just return a string representation # if it's some mystery object, just return a string representation
except TypeError: except TypeError:
return str(o) return str(o)
@ -178,10 +187,22 @@ def initialise_file(config_path, populate: str = "{}\n"):
outfile.write(populate) outfile.write(populate)
# Load the default config # Load the default settings
with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc: with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
DEFAULT_CONFIG = default_rc.read() DEFAULT_SETTINGS = default_settings.read()
#: Default user settings object #: 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 pkg_resources
import uuid import uuid
from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.stage.mock import MissingStage
from openflexure_microscope.stage.mock import MockStage from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.stage.sanga import SangaStage
from openflexure_microscope.camera.mock import MockStreamer
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.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.lock import CompositeLock
from labthings.core.utilities import rupdate
class Microscope: class Microscope:
@ -24,20 +30,26 @@ class Microscope:
The camera and stage objects may already be initialised, and can be passed as arguments. The camera and stage objects may already be initialised, and can be passed as arguments.
""" """
def __init__(self): def __init__(self, settings=user_settings, configuration=user_configuration):
# Initial attributes self.id = uuid.uuid4()
self.id = uuid.uuid4() #: Microscope UUID self.name = self.id
self.name = self.id #: Microscope name (modifiable)
self.fov = [0, 0] #: Microscope field-of-view in stage motor steps 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 # Store settings and configuration files
self.settings_file = settings
self.configuration_file = configuration
# Initialise with an empty composite lock # Initialise with an empty composite lock
#: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage #: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage
self.lock = CompositeLock([]) 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 # Apply settings loaded from file
self.apply_settings(user_settings.load()) self.update_settings(self.settings_file.load())
def __enter__(self): def __enter__(self):
"""Create microscope on context enter.""" """Create microscope on context enter."""
@ -56,58 +68,45 @@ class Microscope:
self.stage.close() self.stage.close()
logging.info("Closed {}".format(self)) 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. Attach microscope components based on initially passed configuration file
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
""" """
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...") ### Stage
self.camera = ( if configuration.get("stage"):
camera stage_type = configuration["stage"].get("type")
) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object stage_port = configuration["stage"].get("port")
if stage_type in ("SangaBoard", "SangaStage"):
self.stage = SangaStage(port=stage_port)
### Fallbacks
if not self.camera: if not self.camera:
logging.info("No camera attached.") self.camera = MissingCamera()
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
if not self.stage: if not self.stage:
logging.info("No stage attached.") self.stage = MissingStage()
else:
logging.info("Attached stage {}".format(stage))
if hasattr(self.stage, "lock"): # If stage object has a lock ### Locks
logging.info( if hasattr(self.camera, "lock"):
"Attaching lock {} to composite lock.".format(self.stage.lock) self.lock.locks.append(self.camera.lock)
) if hasattr(self.stage, "lock"):
# Add the lock to the microscope composite lock self.lock.locks.append(self.stage.lock)
self.lock.locks.append(self.stage.lock)
logging.info("Reapplying settings to newly attached devices")
self.apply_settings(settings_full)
def has_real_stage(self) -> bool: def has_real_stage(self) -> bool:
""" """
Check if a real (non-mock) stage is currently attached. 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 return True
else: else:
return False return False
@ -116,49 +115,45 @@ class Microscope:
""" """
Check if a real (non-mock) camera is currently attached. 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 return True
else: else:
return False return False
# Create unified status # Create unified state
@property @property
def status(self): def state(self):
"""Dictionary of the basic microscope status. """Dictionary of the basic microscope state.
Return: Return:
dict: Dictionary containing complete microscope status dict: Dictionary containing complete microscope state
""" """
state = { state = {"camera": self.camera.state, "stage": self.stage.state}
"camera": self.camera.status,
"stage": self.stage.status,
"version": pkg_resources.get_distribution("openflexure_microscope").version,
}
return 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. 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 attached to a camera
if ("camera_settings" in config) and self.camera: if ("camera" in settings) and self.camera:
self.camera.apply_settings(config["camera_settings"]) self.camera.update_settings(settings["camera"])
# If attached to a stage # If attached to a stage
if ("stage_settings" in config) and self.stage: if ("stage" in settings) and self.stage:
self.stage.apply_settings(config["stage_settings"]) self.stage.update_settings(settings["stage"])
# Todo: tidy up with some loopy goodness # Microscope settings
if "id" in config: if "id" in settings:
self.id = config["id"] self.id = settings["id"]
if "name" in config: if "name" in settings:
self.name = config["name"] self.name = settings["name"]
if "fov" in config: if "fov" in settings:
self.fov = config["fov"] self.fov = settings["fov"]
def read_settings(self, json_safe=False): def read_settings(self, full: bool = True):
""" """
Get an updated settings dictionary. Get an updated settings dictionary.
@ -174,16 +169,34 @@ class Microscope:
# If attached to a camera # If attached to a camera
if self.camera: if self.camera:
settings_current_camera = self.camera.read_settings() 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:
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 attached to a stage
if self.stage: if self.stage:
settings_current_stage = self.stage.read_settings() 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): def save_settings(self):
""" """
@ -196,7 +209,31 @@ class Microscope:
self.camera.save_settings() self.camera.save_settings()
if self.stage: if self.stage:
self.stage.save_settings() 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 @property
def metadata(self): def metadata(self):
@ -204,23 +241,10 @@ class Microscope:
Microscope system metadata, to be applied to basically all captures Microscope system metadata, to be applied to basically all captures
""" """
system_metadata = { system_metadata = {
"microscope_settings": self.read_settings(), "id": self.id,
"microscope_state": self.status, "settings": self.read_settings(full=False),
"microscope_id": self.id, "state": self.state,
"microscope_name": self.name, "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 return system_metadata

View file

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

View file

@ -1,7 +1,3 @@
{ {
"fov": [ "jpeg_quality": 100
4100,
3146
],
"jpeg_quality": 75
} }

View file

@ -32,8 +32,9 @@ def logs_file_path(filename: str):
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
#: Path of default (first-run) microscope settings #: 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")
# BASE PATHS # BASE PATHS
@ -60,7 +61,9 @@ else:
# SERVER PATHS # SERVER PATHS
#: Path of microscope settings directory #: Path of microscope settings file
CONFIG_FILE_PATH = settings_file_path("microscope_settings.json") 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 #: Path of microscope extensions directory
OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions") OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")

View file

@ -14,7 +14,7 @@ class BaseStage(metaclass=ABCMeta):
self.lock = StrictLock(timeout=5) self.lock = StrictLock(timeout=5)
@abstractmethod @abstractmethod
def apply_settings(self, config: dict): def update_settings(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
pass pass
@ -29,12 +29,14 @@ class BaseStage(metaclass=ABCMeta):
@property @property
@abstractmethod @abstractmethod
def status(self): def state(self):
"""The general state dictionary of the board. """The general state dictionary of the board."""
Should at least contain 'position', and 'board' keys. pass
Note: A None/Null value for 'board' will disable stage
movement in the OpenFlexure eV client software, @property
""" @abstractmethod
def configuration(self):
"""The general stage configuration."""
pass pass
@property @property

View file

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

View file

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

537
poetry.lock generated
View file

@ -14,14 +14,6 @@ optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "3.2.0" version = "3.2.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)"]
tests = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"]
validation = ["prance (>=0.11)"]
yaml = ["PyYAML (>=3.10)"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
@ -55,12 +47,6 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0" version = "19.3.0"
[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "Internationalization utilities" description = "Internationalization utilities"
@ -86,9 +72,6 @@ attrs = ">=17.4.0"
click = ">=6.5" click = ">=6.5"
toml = ">=0.9.4" toml = ">=0.9.4"
[package.extras]
d = ["aiohttp (>=3.3.2)"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
@ -144,11 +127,6 @@ Werkzeug = ">=0.15"
click = ">=5.1" click = ">=5.1"
itsdangerous = ">=0.24" itsdangerous = ">=0.24"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
dotenv = ["python-dotenv"]
[[package]] [[package]]
category = "main" category = "main"
description = "A Flask extension adding a decorator for CORS support" description = "A Flask extension adding a decorator for CORS support"
@ -185,12 +163,6 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "4.3.21" version = "4.3.21"
[package.extras]
pipfile = ["pipreqs", "requirementslib"]
pyproject = ["toml"]
requirements = ["pipreqs", "pip-api"]
xdg_home = ["appdirs (>=1.4.0)"]
[[package]] [[package]]
category = "main" category = "main"
description = "Various helpers to pass data to untrusted environments and back." description = "Various helpers to pass data to untrusted environments and back."
@ -204,15 +176,12 @@ category = "main"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
name = "jinja2" name = "jinja2"
optional = false optional = false
python-versions = "*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.10.3" version = "2.11.0"
[package.dependencies] [package.dependencies]
MarkupSafe = ">=0.23" MarkupSafe = ">=0.23"
[package.extras]
i18n = ["Babel (>=0.8)"]
[[package]] [[package]]
category = "main" category = "main"
description = "" description = ""
@ -229,7 +198,7 @@ marshmallow = "^3.3.0"
webargs = "^5.5.2" webargs = "^5.5.2"
[package.source] [package.source]
reference = "23732b17074486367541439a1caa2eb61b9cc22f" reference = "cb1ef6a1f8849f4d58601a7a9617ffea12703f39"
type = "git" type = "git"
url = "https://github.com/labthings/python-labthings.git" url = "https://github.com/labthings/python-labthings.git"
[[package]] [[package]]
@ -256,12 +225,6 @@ optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "3.3.0" version = "3.3.0"
[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)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "McCabe checker, plugin for flake8" description = "McCabe checker, plugin for flake8"
@ -298,11 +261,6 @@ optional = true
python-versions = "*" python-versions = "*"
version = "1.13.1b0" version = "1.13.1b0"
[package.extras]
array = ["numpy"]
doc = ["sphinx"]
test = ["coverage", "pytest", "mock", "pillow", "numpy"]
[package.source] [package.source]
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb" reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
type = "git" type = "git"
@ -353,6 +311,17 @@ optional = false
python-versions = "*" python-versions = "*"
version = "3.4" 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]] [[package]]
category = "dev" category = "dev"
description = "World timezone definitions, modern and historical" description = "World timezone definitions, modern and historical"
@ -383,10 +352,6 @@ chardet = ">=3.0.2,<3.1.0"
idna = ">=2.5,<2.9" idna = ">=2.5,<2.9"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 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)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "a python refactoring library..." description = "a python refactoring library..."
@ -414,6 +379,14 @@ version = "1.3.0"
[package.dependencies] [package.dependencies]
numpy = ">=1.13.3" 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]] [[package]]
category = "main" category = "main"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
@ -457,10 +430,6 @@ sphinxcontrib-jsmath = "*"
sphinxcontrib-qthelp = "*" sphinxcontrib-qthelp = "*"
sphinxcontrib-serializinghtml = "*" sphinxcontrib-serializinghtml = "*"
[package.extras]
docs = ["sphinxcontrib-websupport"]
test = ["pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = ""
@ -469,9 +438,6 @@ optional = false
python-versions = "*" python-versions = "*"
version = "1.0.1" version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = ""
@ -480,9 +446,6 @@ optional = false
python-versions = "*" python-versions = "*"
version = "1.0.1" version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = ""
@ -491,9 +454,6 @@ optional = false
python-versions = "*" python-versions = "*"
version = "1.0.2" version = "1.0.2"
[package.extras]
test = ["pytest", "flake8", "mypy", "html5lib"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "Sphinx domain for documenting HTTP APIs" description = "Sphinx domain for documenting HTTP APIs"
@ -514,9 +474,6 @@ optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "1.0.1" version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = ""
@ -525,9 +482,6 @@ optional = false
python-versions = "*" python-versions = "*"
version = "1.0.2" version = "1.0.2"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = ""
@ -536,9 +490,6 @@ optional = false
python-versions = "*" python-versions = "*"
version = "1.1.3" version = "1.1.3"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language" description = "Python Library for Tom's Obvious, Minimal Language"
@ -564,28 +515,17 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.8" version = "1.25.8"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]] [[package]]
category = "main" category = "main"
description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp." description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp."
name = "webargs" name = "webargs"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "5.5.2" version = "5.5.3"
[package.dependencies] [package.dependencies]
marshmallow = ">=2.15.2" 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)"]
docs = ["Sphinx (2.2.0)", "sphinx-issues (1.2.0)", "sphinx-typlog-theme (0.7.3)", "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)", "aiohttp (>=3.0.0)"]
frameworks = ["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)", "aiohttp (>=3.0.0)"]
lint = ["flake8 (3.7.8)", "pre-commit (>=1.17,<2.0)", "mypy (0.730)", "flake8-bugbear (19.8.0)"]
tests = ["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)", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "aiohttp (>=3.0.0)"]
[[package]] [[package]]
category = "main" category = "main"
@ -593,12 +533,7 @@ description = "The comprehensive WSGI web application library."
name = "werkzeug" name = "werkzeug"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.16.0" version = "0.16.1"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
termcolor = ["termcolor"]
watchdog = ["watchdog"]
[[package]] [[package]]
category = "dev" category = "dev"
@ -612,373 +547,63 @@ version = "1.11.2"
rpi = ["picamera", "RPi.GPIO"] rpi = ["picamera", "RPi.GPIO"]
[metadata] [metadata]
content-hash = "cd4e5d59e9a63ae053a05c994a61a46be87a29ec6867bcd47f6ef9b39bb785d3" content-hash = "34da76b3341563a0bebfa3e4a3fca90f34b857027e90811fed92b106ad06c2ba"
python-versions = "^3.6" python-versions = "^3.6"
[metadata.files] [metadata.hashes]
alabaster = [ alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"]
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, apispec = ["c1625ae910f699c9adb21928693a3245b9faab6843f1b1c94ab2d505549cd78a", "f2ecb3a6534cfb42cf69b5c1222d2c0e695aab5ec3d7edde31b7354670948f05"]
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"]
] astroid = ["71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"]
apispec = [ attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
{file = "apispec-3.2.0-py2.py3-none-any.whl", hash = "sha256:f2ecb3a6534cfb42cf69b5c1222d2c0e695aab5ec3d7edde31b7354670948f05"}, babel = ["1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", "d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"]
{file = "apispec-3.2.0.tar.gz", hash = "sha256:c1625ae910f699c9adb21928693a3245b9faab6843f1b1c94ab2d505549cd78a"}, black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"]
] certifi = ["017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"]
appdirs = [ chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"]
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
{file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"]
] docutils = ["0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"]
astroid = [ flask = ["13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", "45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"]
{file = "astroid-2.3.3-py3-none-any.whl", hash = "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"}, flask-cors = ["72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", "f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"]
{file = "astroid-2.3.3.tar.gz", hash = "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a"}, idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"]
] imagesize = ["6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"]
attrs = [ isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"]
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"]
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, jinja2 = ["6e7a3c2934694d59ad334c93dd1b6c96699cf24c53fdb8ec848ac6b23e685734", "d6609ae5ec3d56212ca7d802eda654eaf2310000816ce815361041465b108be4"]
]
babel = [
{file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"},
{file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"},
]
black = [
{file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"},
{file = "black-18.9b0.tar.gz", hash = "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"},
]
certifi = [
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
{file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
]
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"},
]
colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
docutils = [
{file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"},
{file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"},
]
flask = [
{file = "Flask-1.1.1-py2.py3-none-any.whl", hash = "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"},
{file = "Flask-1.1.1.tar.gz", hash = "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52"},
]
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"},
]
idna = [
{file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"},
{file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"},
]
imagesize = [
{file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
]
isort = [
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
{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"},
]
labthings = [] labthings = []
lazy-object-proxy = [ lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"]
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, marshmallow = ["0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e", "3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"]
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, numpy = ["1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6", "17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e", "20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc", "2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc", "39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a", "56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa", "590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3", "70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121", "77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971", "9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26", "9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd", "ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480", "b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec", "b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77", "b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57", "c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07", "cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572", "d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73", "e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca", "e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474", "f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"]
{file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, packaging = ["170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", "e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"]
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"},
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"},
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"},
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"},
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"},
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"},
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"},
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"},
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"},
{file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"},
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{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"},
]
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"},
]
packaging = [
{file = "packaging-20.1-py2.py3-none-any.whl", hash = "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73"},
{file = "packaging-20.1.tar.gz", hash = "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"},
]
picamera = [] picamera = []
pillow = [ pillow = ["01a501be4ae05fd714d269cb9c9f145518e58e73faa3f140ddb67fae0c2607b1", "051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", "07c35919f983c2c593498edcc126ad3a94154184899297cc9d27a6587672cbaa", "0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", "0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", "0cf0208500df8d0c3cad6383cd98a2d038b0678fd4f777a8f7e442c5faeee81d", "163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", "18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", "24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", "267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", "3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", "39fbd5d62167197318a0371b2a9c699ce261b6800bb493eadde2ba30d868fe8c", "4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d", "4baab2d2da57b0d9d544a2ce0f461374dd90ccbcf723fe46689aff906d43a964", "4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", "4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", "505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", "52e2e56fc3706d8791761a157115dc8391319720ad60cc32992350fda74b6be2", "5337ac3280312aa065ed0a8ec1e4b6142e9f15c31baed36b5cd964745853243f", "5ccd97e0f01f42b7e35907272f0f8ad2c3660a482d799a0c564c7d50e83604d4", "5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", "634209852cc06c0c1243cc74f8fdc8f7444d866221de51125f7b696d775ec5ca", "75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522", "7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", "801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", "825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", "87fe838f9dac0597f05f2605c0700b1926f9390c95df6af45d83141e0c514bd9", "9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", "a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", "a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", "a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", "a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", "ac036b6a6bac7010c58e643d78c234c2f7dc8bb7e591bd8bc3555cf4b1527c28", "b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", "ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", "ba6ef2bd62671c7fb9cdb3277414e87a5cd38b86721039ada1464f7452ad30b2", "c8939dba1a37960a502b1a030a4465c46dd2c2bca7adf05fa3af6bea594e720e", "cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", "cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", "d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", "d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", "db418635ea20528f247203bf131b40636f77c8209a045b89fa3badb89e1fcea0", "e1555d4fda1db8005de72acf2ded1af660febad09b4708430091159e8ae1963e", "e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", "e9f13711780c981d6eadd6042af40e172548c54b06266a1aabda7de192db0838", "f0e3288b92ca5dbb1649bd00e80ef652a72b657dc94989fa9c348253d179054b", "f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", "f62b1aeb5c2ced8babd4fbba9c74cbef9de309f5ed106184b12d9778a3971f15", "f71ff657e63a9b24cac254bb8c9bd3c89c7a1b5e00ee4b3997ca1c18100dac28", "fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"]
{file = "Pillow-5.4.1-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1"}, pygments = ["2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", "98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"]
{file = "Pillow-5.4.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7"}, pylint = ["3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"]
{file = "Pillow-5.4.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e"}, pyparsing = ["4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", "c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"]
{file = "Pillow-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c"}, pyserial = ["6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", "e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"]
{file = "Pillow-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f"}, python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"]
{file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c"}, pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"]
{file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc"}, pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"]
{file = "Pillow-5.4.1-cp34-cp34m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1"}, requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
{file = "Pillow-5.4.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8"}, rope = ["6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", "f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"]
{file = "Pillow-5.4.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055"}, "rpi.gpio" = ["a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"]
{file = "Pillow-5.4.1-cp34-cp34m-win32.whl", hash = "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6"}, scipy = ["03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351", "09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e", "10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e", "1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba", "409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb", "4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340", "6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1", "826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7", "a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae", "a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06", "adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef", "b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785", "c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d", "c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a", "c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921", "db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"]
{file = "Pillow-5.4.1-cp34-cp34m-win_amd64.whl", hash = "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf"}, simplejson = ["0fe3994207485efb63d8f10a833ff31236ed27e3b23dadd0bf51c9900313f8f2", "17163e643dbf125bb552de17c826b0161c68c970335d270e174363d19e7ea882", "1d1e929cdd15151f3c0b2efe953b3281b2fd5ad5f234f77aca725f28486466f6", "1d346c2c1d7dd79c118f0cc7ec5a1c4127e0c8ffc83e7b13fc5709ff78c9bb84", "1ea59f570b9d4916ae5540a9181f9c978e16863383738b69a70363bc5e63c4cb", "1fbba86098bbfc1f85c5b69dc9a6d009055104354e0d9880bb00b692e30e0078", "229edb079d5dd81bf12da952d4d825bd68d1241381b37d3acf961b384c9934de", "22a7acb81968a7c64eba7526af2cf566e7e2ded1cb5c83f0906b17ff1540f866", "2b4b2b738b3b99819a17feaf118265d0753d5536049ea570b3c43b51c4701e81", "4cf91aab51b02b3327c9d51897960c554f00891f9b31abd8a2f50fd4a0071ce8", "4fd5f79590694ebff8dc980708e1c182d41ce1fda599a12189f0ca96bf41ad70", "5cfd495527f8b85ce21db806567de52d98f5078a8e9427b18e251c68bd573a26", "60aad424e47c5803276e332b2a861ed7a0d46560e8af53790c4c4fb3420c26c2", "7739940d68b200877a15a5ff5149e1599737d6dd55e302625650629350466418", "7cce4bac7e0d66f3a080b80212c2238e063211fe327f98d764c6acbc214497fc", "8027bd5f1e633eb61b8239994e6fc3aba0346e76294beac22a892eb8faa92ba1", "86afc5b5cbd42d706efd33f280fec7bd7e2772ef54e3f34cf6b30777cd19a614", "87d349517b572964350cc1adc5a31b493bbcee284505e81637d0174b2758ba17", "8de378d589eccbc75941e480b4d5b4db66f22e4232f87543b136b1f093fff342", "926bcbef9eb60e798eabda9cd0bbcb0fca70d2779aa0aa56845749d973eb7ad5", "9a126c3a91df5b1403e965ba63b304a50b53d8efc908a8c71545ed72535374a3", "ad8dd3454d0c65c0f92945ac86f7b9efb67fa2040ba1b0189540e984df904378", "d140e9376e7f73c1f9e0a8e3836caf5eec57bbafd99259d56979da05a6356388", "da00675e5e483ead345429d4f1374ab8b949fba4429d60e71ee9d030ced64037", "daaf4d11db982791be74b23ff4729af2c7da79316de0bebf880fa2d60bcc8c5a", "f4b64a1031acf33e281fd9052336d6dad4d35eee3404c95431c8c6bc7a9c0588", "fc046afda0ed8f5295212068266c92991ab1f4a50c6a7144b69364bdee4a0159", "fc9051d249dd5512e541f20330a74592f7a65b2d62e18122ca89bf71f94db748"]
{file = "Pillow-5.4.1-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239"}, six = ["236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", "8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"]
{file = "Pillow-5.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3"}, snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"]
{file = "Pillow-5.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697"}, sphinx = ["298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159", "e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5"]
{file = "Pillow-5.4.1-cp35-cp35m-win32.whl", hash = "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7"}, sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"]
{file = "Pillow-5.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe"}, sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"]
{file = "Pillow-5.4.1-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca"}, sphinxcontrib-htmlhelp = ["4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"]
{file = "Pillow-5.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"}, sphinxcontrib-httpdomain = ["1fb5375007d70bf180cdd1c79e741082be7aa2d37ba99efe561e1c2e3f38191e", "ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"]
{file = "Pillow-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad"}, sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"]
{file = "Pillow-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a"}, sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"]
{file = "Pillow-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356"}, sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"]
{file = "Pillow-5.4.1-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1"}, toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
{file = "Pillow-5.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9"}, typed-ast = ["0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"]
{file = "Pillow-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3"}, urllib3 = ["2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"]
{file = "Pillow-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f"}, webargs = ["4f04918864c7602886335d8099f9b8960ee698b6b914f022736ed50be6b71235", "871642a2e0c62f21d5b78f357750ac7a87e6bc734c972f633aa5fb6204fbf29a", "fc81c9f9d391acfbce406a319217319fd8b2fd862f7fdb5319ad06944f36ed25"]
{file = "Pillow-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3"}, werkzeug = ["1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2", "b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04"]
{file = "Pillow-5.4.1-pp260-pypy_41-win32.whl", hash = "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494"}, wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"]
{file = "Pillow-5.4.1-pp360-pp360-win32.whl", hash = "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000"},
{file = "Pillow-5.4.1-py2.7-macosx-10.13-x86_64.egg", hash = "sha256:634209852cc06c0c1243cc74f8fdc8f7444d866221de51125f7b696d775ec5ca"},
{file = "Pillow-5.4.1-py2.7-win-amd64.egg", hash = "sha256:0cf0208500df8d0c3cad6383cd98a2d038b0678fd4f777a8f7e442c5faeee81d"},
{file = "Pillow-5.4.1-py2.7-win32.egg", hash = "sha256:f71ff657e63a9b24cac254bb8c9bd3c89c7a1b5e00ee4b3997ca1c18100dac28"},
{file = "Pillow-5.4.1-py3.4-win-amd64.egg", hash = "sha256:01a501be4ae05fd714d269cb9c9f145518e58e73faa3f140ddb67fae0c2607b1"},
{file = "Pillow-5.4.1-py3.4-win32.egg", hash = "sha256:4baab2d2da57b0d9d544a2ce0f461374dd90ccbcf723fe46689aff906d43a964"},
{file = "Pillow-5.4.1-py3.5-win-amd64.egg", hash = "sha256:f62b1aeb5c2ced8babd4fbba9c74cbef9de309f5ed106184b12d9778a3971f15"},
{file = "Pillow-5.4.1-py3.5-win32.egg", hash = "sha256:e9f13711780c981d6eadd6042af40e172548c54b06266a1aabda7de192db0838"},
{file = "Pillow-5.4.1-py3.6-win-amd64.egg", hash = "sha256:07c35919f983c2c593498edcc126ad3a94154184899297cc9d27a6587672cbaa"},
{file = "Pillow-5.4.1-py3.6-win32.egg", hash = "sha256:87fe838f9dac0597f05f2605c0700b1926f9390c95df6af45d83141e0c514bd9"},
{file = "Pillow-5.4.1-py3.7-win-amd64.egg", hash = "sha256:c8939dba1a37960a502b1a030a4465c46dd2c2bca7adf05fa3af6bea594e720e"},
{file = "Pillow-5.4.1-py3.7-win32.egg", hash = "sha256:5337ac3280312aa065ed0a8ec1e4b6142e9f15c31baed36b5cd964745853243f"},
{file = "Pillow-5.4.1.tar.gz", hash = "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f"},
{file = "Pillow-5.4.1.win-amd64-py2.7.exe", hash = "sha256:e1555d4fda1db8005de72acf2ded1af660febad09b4708430091159e8ae1963e"},
{file = "Pillow-5.4.1.win-amd64-py3.4.exe", hash = "sha256:52e2e56fc3706d8791761a157115dc8391319720ad60cc32992350fda74b6be2"},
{file = "Pillow-5.4.1.win-amd64-py3.5.exe", hash = "sha256:ba6ef2bd62671c7fb9cdb3277414e87a5cd38b86721039ada1464f7452ad30b2"},
{file = "Pillow-5.4.1.win-amd64-py3.6.exe", hash = "sha256:39fbd5d62167197318a0371b2a9c699ce261b6800bb493eadde2ba30d868fe8c"},
{file = "Pillow-5.4.1.win-amd64-py3.7.exe", hash = "sha256:5ccd97e0f01f42b7e35907272f0f8ad2c3660a482d799a0c564c7d50e83604d4"},
{file = "Pillow-5.4.1.win32-py2.7.exe", hash = "sha256:db418635ea20528f247203bf131b40636f77c8209a045b89fa3badb89e1fcea0"},
{file = "Pillow-5.4.1.win32-py3.4.exe", hash = "sha256:ac036b6a6bac7010c58e643d78c234c2f7dc8bb7e591bd8bc3555cf4b1527c28"},
{file = "Pillow-5.4.1.win32-py3.5.exe", hash = "sha256:f0e3288b92ca5dbb1649bd00e80ef652a72b657dc94989fa9c348253d179054b"},
{file = "Pillow-5.4.1.win32-py3.6.exe", hash = "sha256:4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d"},
{file = "Pillow-5.4.1.win32-py3.7.exe", hash = "sha256:75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522"},
]
pygments = [
{file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"},
{file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"},
]
pylint = [
{file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"},
{file = "pylint-2.4.4.tar.gz", hash = "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd"},
]
pyparsing = [
{file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"},
{file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"},
]
pyserial = [
{file = "pyserial-3.4-py2.py3-none-any.whl", hash = "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"},
{file = "pyserial-3.4.tar.gz", hash = "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627"},
]
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"},
]
rope = [
{file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"},
{file = "rope-0.14.0-py3-none-any.whl", hash = "sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"},
{file = "rope-0.14.0.tar.gz", hash = "sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf"},
]
"rpi.gpio" = [
{file = "RPi.GPIO-0.6.5.tar.gz", hash = "sha256:a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"},
]
scipy = [
{file = "scipy-1.3.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340"},
{file = "scipy-1.3.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba"},
{file = "scipy-1.3.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef"},
{file = "scipy-1.3.0-cp35-cp35m-win32.whl", hash = "sha256:03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351"},
{file = "scipy-1.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae"},
{file = "scipy-1.3.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7"},
{file = "scipy-1.3.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785"},
{file = "scipy-1.3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"},
{file = "scipy-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb"},
{file = "scipy-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d"},
{file = "scipy-1.3.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e"},
{file = "scipy-1.3.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06"},
{file = "scipy-1.3.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921"},
{file = "scipy-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1"},
{file = "scipy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e"},
{file = "scipy-1.3.0.tar.gz", hash = "sha256:c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a"},
]
six = [
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
{file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
]
snowballstemmer = [
{file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
{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"},
]
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"},
]
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"},
]
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"},
]
sphinxcontrib-httpdomain = [
{file = "sphinxcontrib-httpdomain-1.7.0.tar.gz", hash = "sha256:ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"},
{file = "sphinxcontrib_httpdomain-1.7.0-py2.py3-none-any.whl", hash = "sha256:1fb5375007d70bf180cdd1c79e741082be7aa2d37ba99efe561e1c2e3f38191e"},
]
sphinxcontrib-jsmath = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
{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"},
]
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"},
]
toml = [
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
{file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
{file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
]
typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
urllib3 = [
{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"},
]
werkzeug = [
{file = "Werkzeug-0.16.0-py2.py3-none-any.whl", hash = "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"},
{file = "Werkzeug-0.16.0.tar.gz", hash = "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7"},
]
wrapt = [
{file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"},
]

View file

@ -35,6 +35,7 @@ picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shadi
pyserial = "^3.4" # Used for sangaboard (basic_serial_instrument) until we move to sangaboard pip library 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 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"
[tool.poetry.extras] [tool.poetry.extras]
rpi = ["picamera", "RPi.GPIO"] rpi = ["picamera", "RPi.GPIO"]