Moved to a LabThings dependency for server

This commit is contained in:
jtc42 2020-01-17 15:13:32 +00:00
parent a1223d8b28
commit 36334ed743
65 changed files with 546 additions and 2364 deletions

View file

@ -10,6 +10,8 @@ if "%SPHINXBUILD%" == "" (
set SOURCEDIR=source
set BUILDDIR=build
set SPHINXOPTS="-E"
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL

View file

@ -3,5 +3,5 @@ Classes and Modules
Extension class
---------------
.. autoclass:: openflexure_microscope.common.flask_labthings.extensions.BaseExtension
.. autoclass:: labthings.server.extensions.BaseExtension
:members:

View file

@ -2,7 +2,7 @@ from openflexure_microscope.plugins import MicroscopePlugin
from openflexure_microscope.api.views import MicroscopeViewPlugin
from openflexure_microscope.api.utilities import JsonResponse
from openflexure_microscope.common.labthings_core.tasks import taskify
from labthings.core.tasks import taskify
import os
import time

View file

@ -21,7 +21,7 @@ Tasks are introduced to manage long-running functions in a way that does not blo
the use of tasks, long-running functions would block an HTTP response until the function returns, often
resulting in a timeout.
To prevent this, any function can be offloaded to a background task. This is done through the :py:meth:`openflexure_microscope.common.tasks.taskify`
To prevent this, any function can be offloaded to a background task. This is done through the :py:meth:`labthings.tasks.taskify`
function, by calling ``taskify(<long_running_function>)(*args, **kwargs)``. Internally, the ``tasks`` submodule stores a list
of all requested tasks, which themselves contain a ``state`` dictionary. This dictionary stores the status of the task (if it
is idle, running, error, or success), information about the start and end times, a unique task ID, and the return value of
@ -38,7 +38,7 @@ An example of a long running task may look like:
.. code-block:: python
from openflexure_microscope.plugins import MicroscopeViewPlugin
from openflexure_microscope.common.tasks import taskify
from labthings.tasks import taskify
class MyPlugin(MicroscopeViewPlugin):
def post(self):
@ -63,7 +63,7 @@ the microscope hardware. For example, even if the stage is not actively moving (
within a tile scan), another user should not be able to move the microscope, interrupting the task. Thread locks act
to prevent this.
The camera and stage both contain an instance of :py:class:`openflexure_microscope.common.lock.StrictLock`, named ``lock``.
The camera and stage both contain an instance of :py:class:`labthings.lock.StrictLock`, named ``lock``.
Built-in functions such as capture and move will always acquire this lock for the duration of the function. This ensures
that, for example, simultaneous attemps to move do not occur.
@ -77,7 +77,7 @@ For example, a timelapse plugin may look like:
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
from openflexure_microscope.api.utilities import JsonResponse
from openflexure_microscope.common.tasks import taskify
from labthings.tasks import taskify
### MICROSCOPE PLUGIN ###

View file

@ -1,17 +1,17 @@
Basic extension structure
=========================
An extension starts as a simple instance of :py:class:`openflexure_microscope.common.flask_labthings.extensions.BaseExtension`.
An extension starts as a simple instance of :py:class:`labthings.server.extensions.BaseExtension`.
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
In order to access the currently running microscope object, use the :py:func:`openflexure_microscope.common.flask_labthings.find` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
In order to access the currently running microscope object, use the :py:func:`labthings.server.find` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
A simple extension file, with no API views but application-available methods may look like:
.. code-block:: python
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
from openflexure_microscope.common.flask_labthings.find import find_component
from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component
def identify():
@ -50,7 +50,7 @@ Once this extension is loaded, any other extensions will have access to your met
.. code-block:: python
from openflexure_microscope.common.flask_labthings.find import find_extension
from labthings.server.find import find_extension
def test_extension_method():
# Find your extension. Returns None if it hasn't been found.

View file

@ -4,4 +4,3 @@ __version__ = "0.1.0"
from .microscope import Microscope
from . import config
from . import utilities
from . import common

View file

@ -22,8 +22,8 @@ from openflexure_microscope.paths import (
settings_file_path,
)
from openflexure_microscope.common.flask_labthings.quick import create_app
from openflexure_microscope.common.flask_labthings.extensions import find_extensions
from labthings.server.quick import create_app
from labthings.server.extensions import find_extensions
from openflexure_microscope.api.microscope import default_microscope as api_microscope

View file

@ -1,7 +1,7 @@
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.find import find_component
from labthings.server.extensions import BaseExtension
from labthings.server.view import View
from labthings.server.decorators import (
ThingAction,
ThingProperty,
)

View file

@ -1,12 +1,12 @@
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.view import View
from labthings.server.find import find_component
from labthings.server.extensions import BaseExtension
from labthings.server.decorators import (
marshal_task,
ThingAction,
)
from openflexure_microscope.common.labthings_core.tasks import taskify
from labthings.core.tasks import taskify
from flask import abort

View file

@ -5,21 +5,21 @@ from typing import Tuple
from functools import reduce
from openflexure_microscope.camera.base import generate_basename
from openflexure_microscope.common.flask_labthings.find import (
from labthings.server.find import (
find_component,
find_extension,
)
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.extensions import BaseExtension
from labthings.server.decorators import (
marshal_task,
use_args,
ThingAction,
)
from openflexure_microscope.common.flask_labthings import fields
from labthings.server import fields
from openflexure_microscope.devel import taskify, abort, update_task_progress
from openflexure_microscope.common.flask_labthings.view import View
from labthings.server.view import View
import time

View file

@ -14,10 +14,10 @@ import zipfile
import tempfile
import logging
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.find import find_component
from labthings.server.view import View
from labthings.server.extensions import BaseExtension
from labthings.server.decorators import (
ThingAction,
ThingProperty,
)

View file

@ -1,6 +1,6 @@
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.view import View
from labthings.server.extensions import BaseExtension
from labthings.server.decorators import (
ThingAction,
ThingProperty,
)

View file

@ -4,10 +4,10 @@ Top-level representation of enabled actions
from . import camera, stage, system
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.find import current_labthing
from openflexure_microscope.common.flask_labthings.utilities import description_from_view
from openflexure_microscope.common.flask_labthings.decorators import Tag
from labthings.server.view import View
from labthings.server.find import current_labthing
from labthings.server.utilities import description_from_view
from labthings.server.decorators import Tag
_actions = {
"capture": {

View file

@ -1,7 +1,7 @@
from openflexure_microscope.api.utilities import get_bool, JsonResponse
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.view import View
from labthings.server.find import find_component
from labthings.server.decorators import (
use_args,
marshal_with,
doc,
@ -10,7 +10,7 @@ from openflexure_microscope.common.flask_labthings.decorators import (
doc_response,
)
from openflexure_microscope.common.flask_labthings import fields
from labthings.server import fields
from openflexure_microscope.utilities import filter_dict
from openflexure_microscope.api.v2.views.captures import capture_schema

View file

@ -1,13 +1,13 @@
from openflexure_microscope.api.utilities import JsonResponse
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.view import View
from labthings.server.find import find_component
from labthings.server.decorators import (
use_args,
marshal_with,
doc,
ThingAction,
)
from openflexure_microscope.common.flask_labthings import fields
from labthings.server import fields
from openflexure_microscope.utilities import axes_to_array, filter_dict

View file

@ -1,9 +1,9 @@
from openflexure_microscope.common.flask_labthings.view import View
from labthings.server.view import View
import subprocess
import os
from sys import platform
from openflexure_microscope.common.flask_labthings.decorators import (
from labthings.server.decorators import (
ThingAction,
doc_response,
)

View file

@ -3,15 +3,15 @@ from flask import abort, request, redirect, url_for, send_file, jsonify
from openflexure_microscope.api.utilities import get_bool, JsonResponse
from openflexure_microscope.common.flask_labthings.schema import Schema
from openflexure_microscope.common.flask_labthings import fields
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.utilities import (
from labthings.server.schema import Schema
from labthings.server import fields
from labthings.server.view import View
from labthings.server.utilities import (
description_from_view,
)
from openflexure_microscope.common.flask_labthings.decorators import marshal_with, doc_response, Tag, ThingProperty
from labthings.server.decorators import marshal_with, doc_response, Tag, ThingProperty
from openflexure_microscope.common.flask_labthings.find import find_component
from labthings.server.find import find_component
from marshmallow import pre_dump

View file

@ -1,15 +1,15 @@
from openflexure_microscope.api.utilities import JsonResponse
from openflexure_microscope.common.labthings_core.utilities import (
from labthings.core.utilities import (
get_by_path,
set_by_path,
create_from_path,
)
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.view import View
from labthings.server.find import find_component
from labthings.server.view import View
from openflexure_microscope.common.flask_labthings.decorators import ThingProperty, Tag, doc_response
from labthings.server.decorators import ThingProperty, Tag, doc_response
from flask import jsonify, request, abort
import logging

View file

@ -1,14 +1,14 @@
from openflexure_microscope.api.utilities import gen, JsonResponse
from openflexure_microscope.common.labthings_core.utilities import (
from labthings.core.utilities import (
get_by_path,
set_by_path,
create_from_path,
)
from openflexure_microscope.common.flask_labthings.find import find_component
from openflexure_microscope.common.flask_labthings.view import View
from openflexure_microscope.common.flask_labthings.decorators import doc_response, ThingProperty
from labthings.server.find import find_component
from labthings.server.view import View
from labthings.server.decorators import doc_response, ThingProperty
from flask import Response

View file

@ -10,7 +10,7 @@ from abc import ABCMeta, abstractmethod
from .capture import CaptureObject
from openflexure_microscope.utilities import entry_by_uuid
from openflexure_microscope.common.labthings_core.lock import StrictLock
from labthings.core.lock import StrictLock
from openflexure_microscope.paths import data_file_path
@ -95,7 +95,7 @@ class BaseCamera(metaclass=ABCMeta):
Attributes:
thread: Background thread reading frames from camera
camera: Camera object
lock (:py:class:`openflexure_microscope.common.lock.StrictLock`): Strict lock controlling thread
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

View file

@ -1,2 +0,0 @@
from . import flask_labthings
from .labthings_core import tasks, lock

View file

@ -1 +0,0 @@
EXTENSION_NAME = "flask-labthings"

View file

@ -1,178 +0,0 @@
from webargs import flaskparser
from functools import wraps, update_wrapper
from flask import make_response
from http import HTTPStatus
from openflexure_microscope.common.labthings_core.utilities import rupdate
from .spec import update_spec
from .schema import TaskSchema
def unpack(value):
"""Return a three tuple of data, code, and headers"""
if not isinstance(value, tuple):
return value, 200, {}
try:
data, code, headers = value
return data, code, headers
except ValueError:
pass
try:
data, code = value
return data, code, {}
except ValueError:
pass
return value, 200, {}
class marshal_with(object):
def __init__(self, schema, code=200):
"""
:param schema: a dict of whose keys will make up the final
serialized response output
"""
self.schema = schema
self.code = code
def __call__(self, f):
# Pass params to call function attribute for external access
update_spec(f, {"_schema": {self.code: self.schema}})
# Wrapper function
@wraps(f)
def wrapper(*args, **kwargs):
resp = f(*args, **kwargs)
if isinstance(resp, tuple):
data, code, headers = unpack(resp)
return make_response(self.schema.jsonify(data), code, headers)
else:
return make_response(self.schema.jsonify(resp))
return wrapper
def marshal_task(f):
# Pass params to call function attribute for external access
update_spec(f, {"responses": {201: {"description": "Task started successfully"}}})
update_spec(f, {"_schema": {201: TaskSchema()}})
# Wrapper function
@wraps(f)
def wrapper(*args, **kwargs):
resp = f(*args, **kwargs)
if isinstance(resp, tuple):
data, code, headers = unpack(resp)
return make_response(TaskSchema().jsonify(data), code, headers)
else:
return make_response(TaskSchema().jsonify(resp))
return wrapper
def ThingAction(viewcls):
# Pass params to call function attribute for external access
update_spec(viewcls, {"tags": ["actions"]})
update_spec(viewcls, {"_groups": ["actions"]})
return viewcls
thing_action = ThingAction
def ThingProperty(viewcls):
# Pass params to call function attribute for external access
update_spec(viewcls, {"tags": ["properties"]})
update_spec(viewcls, {"_groups": ["properties"]})
return viewcls
thing_property = ThingProperty
class use_args(object):
def __init__(self, schema, **kwargs):
"""
Equivalent to webargs.flask_parser.use_args
"""
self.schema = schema
self.wrapper = flaskparser.use_args(schema, **kwargs)
def __call__(self, f):
# Pass params to call function attribute for external access
update_spec(f, {"_params": self.schema})
# Wrapper function
update_wrapper(self.wrapper, f)
return self.wrapper(f)
class use_kwargs(use_args):
def __init__(self, schema, **kwargs):
"""
Equivalent to webargs.flask_parser.use_kwargs
"""
kwargs["as_kwargs"] = True
use_args.__init__(self, schema, **kwargs)
class Doc(object):
def __init__(self, **kwargs):
self.kwargs = kwargs
def __call__(self, f):
# Pass params to call function attribute for external access
update_spec(f, self.kwargs)
return f
doc = Doc
class Tag(object):
def __init__(self, tags):
if isinstance(tags, str):
self.tags = [tags]
elif isinstance(tags, list) and all([isinstance(e, str) for e in tags]):
self.tags = tags
else:
raise TypeError("Tags must be a string or list of strings")
def __call__(self, f):
# Pass params to call function attribute for external access
update_spec(f, {"tags": self.tags})
return f
tag = Tag
class doc_response(object):
def __init__(self, code, description=None, mimetype=None, **kwargs):
self.code = code
self.description = description
self.kwargs = kwargs
self.mimetype = mimetype
self.response_dict = {
"responses": {
self.code: {
"description": self.description or HTTPStatus(self.code).phrase,
**self.kwargs,
}
}
}
if self.mimetype:
self.response_dict.update({
"responses": {
self.code: {
"content": {self.mimetype: {}}
}
}
})
def __call__(self, f):
# Pass params to call function attribute for external access
update_spec(f, self.response_dict)
return f

View file

@ -1,37 +0,0 @@
from flask import jsonify, escape
from werkzeug.exceptions import default_exceptions
from werkzeug.exceptions import HTTPException
class JSONExceptionHandler(object):
"""
A class to be registered as a Flask error handler,
converts error codes into a JSON response
"""
def __init__(self, app=None):
if app:
self.init_app(app)
def std_handler(self, error):
if isinstance(error, HTTPException):
message = error.description
elif hasattr(error, "message"):
message = error.message
else:
message = str(error)
status_code = error.code if isinstance(error, HTTPException) else 500
response = {"code": status_code, "message": escape(message)}
return jsonify(response), status_code
def init_app(self, app):
self.app = app
self.register(HTTPException)
for code, v in default_exceptions.items():
self.register(code)
def register(self, exception_or_code, handler=None):
self.app.errorhandler(exception_or_code)(handler or self.std_handler)

View file

@ -1,148 +0,0 @@
import logging
import collections
import copy
import traceback
from importlib import util
import sys
import os
import glob
from openflexure_microscope.common.labthings_core.utilities import (
get_docstring,
camel_to_snake,
snake_to_spine,
)
class BaseExtension:
"""
Parent class for all extensions.
Handles binding route views and forms.
"""
# TODO: Allow adding components to extensions
def __init__(self, name: str, description="", version="0.0.0"):
self._views = (
{}
) # Key: Full, Python-safe ID. Val: Original rule, and view class
self._rules = {} # Key: Original rule. Val: View class
self._meta = {} # Extra metadata to add to the extension description
self._cls = str(self) # String description of extension instance
self.actions = []
self.properties = []
self._name = name
self.description = get_docstring(self)
self.version = str(version)
self.methods = {}
@property
def views(self):
return self._views
def add_view(self, view_class, rule, **kwargs):
# Remove all leading slashes from view route
cleaned_rule = rule
while cleaned_rule[0] == "/":
cleaned_rule = cleaned_rule[1:]
# Expand the rule to include extension name
full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule)
view_id = cleaned_rule.replace("/", "_").replace("<", "").replace(">", "")
# Store route information in a dictionary
d = {"rule": full_rule, "view": view_class, "kwargs": kwargs}
# Add view to private views dictionary
self._views[view_id] = d
# Store the rule expansion information
self._rules[rule] = self._views[view_id]
@property
def meta(self):
d = {}
for k, v in self._meta.items():
if callable(v):
d[k] = v()
else:
d[k] = v
return d
def add_meta(self, key, val):
self._meta[key] = val
@property
def name(self):
return self._name
@property
def _name_python_safe(self):
name = camel_to_snake(self._name) # Camel to snake
name = name.replace(" ", "_") # Spaces to snake
return name
@property
def _name_uri_safe(self):
return snake_to_spine(self._name_python_safe)
def add_method(self, method, method_name):
self.methods[method_name] = method
if not hasattr(self, method_name):
setattr(self, method_name, method)
else:
logging.warning(
"Unable to bind method to extension. Method name already exists."
)
def find_instances_in_module(module, class_to_find):
objs = []
for attribute in dir(module):
if not attribute.startswith("__"):
if isinstance(getattr(module, attribute), class_to_find):
logging.debug(f"Found extension {getattr(module, attribute).name}")
objs.append(getattr(module, attribute))
return objs
def find_extensions_in_file(extension_path: str, module_name="extensions") -> list:
logging.debug(f"Loading extensions from {extension_path}")
spec = util.spec_from_file_location(module_name, extension_path)
mod = util.module_from_spec(spec)
sys.modules[spec.name] = mod
try:
spec.loader.exec_module(mod)
except Exception as e:
logging.error(
f"Exception in extension path {extension_path}: \n{traceback.format_exc()}"
)
return []
else:
if hasattr(mod, "__extensions__"):
return [getattr(mod, ext_name) for ext_name in mod.__extensions__]
else:
return find_instances_in_module(mod, BaseExtension)
def find_extensions(extension_dir: str, module_name="extensions"):
logging.debug(f"Loading extensions from {extension_dir}")
extensions = []
extension_paths = glob.glob(os.path.join(extension_dir, "*.py"))
for extension_path in extension_paths:
extensions.extend(
find_extensions_in_file(extension_path, module_name=module_name)
)
return extensions

View file

@ -1,2 +0,0 @@
# Marshmallow fields
from marshmallow.fields import *

View file

@ -1,50 +0,0 @@
import logging
from flask import current_app
from . import EXTENSION_NAME
def current_labthing():
app = current_app._get_current_object()
if not app:
return None
logging.debug("Active app extensions:")
logging.debug(app.extensions)
logging.debug("Active labthing:")
logging.debug(app.extensions[EXTENSION_NAME])
return app.extensions[EXTENSION_NAME]
def registered_extensions(labthing_instance=None):
if not labthing_instance:
labthing_instance = current_labthing()
return labthing_instance.extensions
def registered_components(labthing_instance=None):
if not labthing_instance:
labthing_instance = current_labthing()
return labthing_instance.components
def find_component(device_name, labthing_instance=None):
if not labthing_instance:
labthing_instance = current_labthing()
if device_name in labthing_instance.components:
return labthing_instance.components[device_name]
else:
return None
def find_extension(extension_name, labthing_instance=None):
if not labthing_instance:
labthing_instance = current_labthing()
logging.debug("Current labthing:")
logging.debug(current_labthing())
if extension_name in labthing_instance.extensions:
return labthing_instance.extensions[extension_name]
else:
return None

View file

@ -1,288 +0,0 @@
from flask import url_for, jsonify
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from . import EXTENSION_NAME # TODO: Move into .names
from .names import TASK_ENDPOINT, TASK_LIST_ENDPOINT, EXTENSION_LIST_ENDPOINT
from .extensions import BaseExtension
from .utilities import description_from_view
from .spec import rule2path, get_spec
from .decorators import tag
from .views.extensions import ExtensionList
from .views.tasks import TaskList, TaskView
from .views.docs import docs_blueprint, SwaggerUIView, W3CThingDescriptionView
from openflexure_microscope.common.labthings_core.utilities import get_docstring
import logging
class LabThing(object):
def __init__(
self,
app=None,
prefix: str = "",
title: str = "",
description: str = "",
version: str = "0.0.0",
):
self.app = app
self.components = {}
self.extensions = {}
self.views = []
self.properties = {}
self.actions = {}
self.custom_root_links = {}
self.endpoints = set()
self.url_prefix = prefix
self._description = description
self._title = title
self._version = version
# Store handlers for things like errors and CORS
self.handlers = {}
self.spec = APISpec(
title=self.title,
version=self.version,
openapi_version="3.0.2",
plugins=[MarshmallowPlugin()],
)
if app is not None:
self.init_app(app)
@property
def description(self,):
return self._description
@description.setter
def description(self, description: str):
self._description = description
self.spec.description = description
@property
def title(self,):
return self._title
@title.setter
def title(self, title: str):
self._title = title
self.spec.title = title
@property
def version(self,):
return str(self._version)
@version.setter
def version(self, version: str):
self._version = version
self.spec.version = version
### Flask stuff
def init_app(self, app):
app.teardown_appcontext(self.teardown)
# Register Flask extension
app.extensions = getattr(app, "extensions", {})
app.extensions[EXTENSION_NAME] = self
# Add resources, if registered before tying to a Flask app
if len(self.views) > 0:
for resource, urls, endpoint, kwargs in self.views:
self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs)
# Create base routes
self._create_base_routes()
def teardown(self, exception):
pass
def _create_base_routes(self):
# Add root representation
self.app.add_url_rule(self._complete_url("/", ""), "rootrep", self.rootrep)
# Add thing descriptions
self.app.register_blueprint(docs_blueprint, url_prefix=self.url_prefix)
# Add extension overview
self.add_view(ExtensionList, "/extensions", endpoint=EXTENSION_LIST_ENDPOINT)
# Add task routes
self.add_view(TaskList, "/tasks", endpoint=TASK_LIST_ENDPOINT)
self.add_view(TaskView, "/tasks/<id>", endpoint=TASK_ENDPOINT)
### Device stuff
def add_component(self, device_object, device_name: str):
self.components[device_name] = device_object
### Extension stuff
def register_extension(self, extension_object):
if isinstance(extension_object, BaseExtension):
self.extensions[extension_object.name] = extension_object
else:
raise TypeError("Extension object must be an instance of BaseExtension")
for extension_view_id, extension_view in extension_object.views.items():
# Add route to the extensions blueprint
self.add_view(
tag("extensions")(extension_view["view"]),
"/extensions" + extension_view["rule"],
**extension_view["kwargs"],
)
### Resource stuff
def _complete_url(self, url_part, registration_prefix):
"""This method is used to defer the construction of the final url in
the case that the Api is created with a Blueprint.
:param url_part: The part of the url the endpoint is registered with
:param registration_prefix: The part of the url contributed by the
blueprint. Generally speaking, BlueprintSetupState.url_prefix
"""
parts = [registration_prefix, self.url_prefix, url_part]
return "".join([part for part in parts if part])
def add_view(self, resource, *urls, endpoint=None, **kwargs):
"""Adds a view to the api.
:param resource: the class name of your resource
:type resource: :class:`Type[Resource]`
:param urls: one or more url routes to match for the resource, standard
flask routing rules apply. Any url variables will be
passed to the resource method as args.
:type urls: str
:param endpoint: endpoint name (defaults to :meth:`Resource.__name__`
Can be used to reference this route in :class:`fields.Url` fields
:type endpoint: str
:param resource_class_args: args to be forwarded to the constructor of
the resource.
:type resource_class_args: tuple
:param resource_class_kwargs: kwargs to be forwarded to the constructor
of the resource.
:type resource_class_kwargs: dict
Additional keyword arguments not specified above will be passed as-is
to :meth:`flask.Flask.add_url_rule`.
Examples::
api.add_resource(HelloWorld, '/', '/hello')
api.add_resource(Foo, '/foo', endpoint="foo")
api.add_resource(FooSpecial, '/special/foo', endpoint="foo")
"""
endpoint = endpoint or resource.__name__.lower()
logging.debug(f"{endpoint}: {type(resource)}")
if self.app is not None:
self._register_view(self.app, resource, *urls, endpoint=endpoint, **kwargs)
self.views.append((resource, urls, endpoint, kwargs))
def view(self, *urls, **kwargs):
def decorator(cls):
self.add_view(cls, *urls, **kwargs)
return cls
return decorator
def _register_view(self, app, view, *urls, endpoint=None, **kwargs):
endpoint = endpoint or view.__name__.lower()
self.endpoints.add(endpoint)
resource_class_args = kwargs.pop("resource_class_args", ())
resource_class_kwargs = kwargs.pop("resource_class_kwargs", {})
# NOTE: 'view_functions' is cleaned up from Blueprint class in Flask 1.0
if endpoint in getattr(app, "view_functions", {}):
previous_view_class = app.view_functions[endpoint].__dict__["view_class"]
# if you override the endpoint with a different class, avoid the collision by raising an exception
if previous_view_class != view:
raise ValueError(
"This endpoint (%s) is already set to the class %s."
% (endpoint, previous_view_class.__name__)
)
view.endpoint = endpoint
resource_func = view.as_view(
endpoint, *resource_class_args, **resource_class_kwargs
)
for url in urls:
# If we've got no Blueprint, just build a url with no prefix
rule = self._complete_url(url, "")
# Add the url to the application or blueprint
app.add_url_rule(rule, view_func=resource_func, **kwargs)
# Add the resource to our API spec
#self.spec.path(**view2path(rule, view, self.spec))
# TEST: Getting Flask rule objects
flask_rules = app.url_map._rules_by_endpoint.get(endpoint)
for flask_rule in flask_rules:
self.spec.path(**rule2path(flask_rule, view, self.spec))
# Handle resource groups listed in API spec
view_spec = get_spec(view)
view_groups = view_spec.get("_groups", {})
if "actions" in view_groups:
self.actions[view.endpoint] = view
if "properties" in view_groups:
self.properties[view.endpoint] = view
### Utilities
def url_for(self, view, **values):
"""Generates a URL to the given resource.
Works like :func:`flask.url_for`."""
endpoint = view.endpoint
return url_for(endpoint, **values)
def owns_endpoint(self, endpoint):
return endpoint in self.endpoints
def add_root_link(self, view, title, kwargs={}):
self.custom_root_links[title] = (view, kwargs)
### Description
def rootrep(self):
"""
Root representation
"""
# TODO: Allow custom root representations
rr = {
"id": url_for("rootrep", _external=True),
"title": self.title,
"description": self.description,
"links": {
"thingDescription": {
"href": url_for("labthings_docs.w3c_td", _external=True),
"description": get_docstring(W3CThingDescriptionView),
},
"swaggerUI": {
"href": url_for("labthings_docs.swagger_ui", _external=True),
**description_from_view(SwaggerUIView),
},
"extensions": {
"href": self.url_for(ExtensionList, _external=True),
**description_from_view(ExtensionList),
},
"tasks": {
"href": self.url_for(TaskList, _external=True),
**description_from_view(TaskList),
},
},
}
for title, (view, kwargs) in self.custom_root_links.items():
rr["links"][title] = {
"href": self.url_for(view, **kwargs, _external=True),
**description_from_view(view),
}
return jsonify(rr)

View file

@ -1,3 +0,0 @@
TASK_ENDPOINT = "labthing_task"
TASK_LIST_ENDPOINT = "labthing_task_list"
EXTENSION_LIST_ENDPOINT = "labthing_extension_list"

View file

@ -1,41 +0,0 @@
from flask import Flask
from flask_cors import CORS
from .labthing import LabThing
from .exceptions import JSONExceptionHandler
def create_app(
import_name,
prefix: str = "",
title: str = "",
description: str = "",
version: str = "0.0.0",
handle_errors: bool = True,
handle_cors: bool = True,
flask_kwargs: dict = {},
):
app = Flask(import_name, **flask_kwargs)
app.url_map.strict_slashes = False
# Handle CORS
if handle_cors:
cors_handler = CORS(app, resources=f"{prefix}/*")
# Handle errors
if handle_errors:
error_handler = JSONExceptionHandler()
error_handler.init_app(app)
# Create a LabThing
labthing = LabThing(
app, prefix=prefix, title=title, description=description, version=str(version)
)
# Store references to added-in handlers
if cors_handler:
labthing.handlers["cors"] = cors_handler
if error_handler:
labthing.handlers["error"] = error_handler
return app, labthing

View file

@ -1,93 +0,0 @@
# -*- coding: utf-8 -*-
from flask import jsonify, url_for
import marshmallow
from .names import TASK_ENDPOINT, TASK_LIST_ENDPOINT, EXTENSION_LIST_ENDPOINT
from .utilities import view_class_from_endpoint, description_from_view
from . import fields
MARSHMALLOW_VERSION_INFO = tuple(
[int(part) for part in marshmallow.__version__.split(".") if part.isdigit()]
)
sentinel = object()
class Schema(marshmallow.Schema):
"""Base serializer with which to define custom serializers.
See `marshmallow.Schema` for more details about the `Schema` API.
"""
def jsonify(self, obj, many=sentinel, *args, **kwargs):
"""Return a JSON response containing the serialized data.
:param obj: Object to serialize.
:param bool many: Whether `obj` should be serialized as an instance
or as a collection. If unset, defaults to the value of the
`many` attribute on this Schema.
:param kwargs: Additional keyword arguments passed to `flask.jsonify`.
.. versionchanged:: 0.6.0
Takes the same arguments as `marshmallow.Schema.dump`. Additional
keyword arguments are passed to `flask.jsonify`.
.. versionchanged:: 0.6.3
The `many` argument for this method defaults to the value of
the `many` attribute on the Schema. Previously, the `many`
argument of this method defaulted to False, regardless of the
value of `Schema.many`.
"""
if many is sentinel:
many = self.many
if MARSHMALLOW_VERSION_INFO[0] >= 3:
data = self.dump(obj, many=many)
else:
data = self.dump(obj, many=many).data
return jsonify(data, *args, **kwargs)
class TaskSchema(Schema):
_ID = fields.String(data_key="id")
target_string = fields.String(data_key="function")
_status = fields.String(data_key="status")
progress = fields.String()
data = fields.Raw()
_return_value = fields.Raw(data_key="return")
_start_time = fields.String(data_key="start_time")
_end_time = fields.String(data_key="end_time")
links = fields.Dict()
@marshmallow.pre_dump
def generate_links(self, data, **kwargs):
data.links = {
"self": {
"href": url_for(TASK_ENDPOINT, id=data.id, _external=True),
"mimetype": "application/json",
**description_from_view(view_class_from_endpoint(TASK_ENDPOINT)),
}
}
return data
class ExtensionSchema(Schema):
name = fields.String(data_key="title")
_name_python_safe = fields.String(data_key="pythonName")
_cls = fields.String(data_key="pythonObject")
meta = fields.Dict()
description = fields.String()
links = fields.Dict()
@marshmallow.pre_dump
def generate_links(self, data, **kwargs):
d = {}
for view_id, view_data in data.views.items():
view_cls = view_data["view"]
view_rule = view_data["rule"]
# Make links dictionary if it doesn't yet exist
d[view_id] = {
"href": url_for(EXTENSION_LIST_ENDPOINT, _external=True) + view_rule,
**description_from_view(view_cls),
}
data.links = d
return data

View file

@ -1,164 +0,0 @@
from ..view import View
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from openflexure_microscope.common.labthings_core.utilities import (
get_docstring,
get_summary,
rupdate,
)
from ..fields import Field
from marshmallow import Schema as BaseSchema
from .paths import rule_to_path, rule_to_params
from werkzeug.routing import Rule
from collections import Mapping
from http import HTTPStatus
def update_spec(obj, spec):
obj.__apispec__ = obj.__dict__.get("__apispec__", {})
rupdate(obj.__apispec__, spec)
return obj.__apispec__
def get_spec(obj):
obj.__apispec__ = obj.__dict__.get("__apispec__", {})
return obj.__apispec__
def rule2path(rule: Rule, view: View, spec: APISpec):
params = {
"path": rule_to_path(rule),
"operations": view2operations(view, spec),
"description": get_docstring(view),
"summary": get_summary(view),
}
# Add URL arguments
if rule.arguments:
for op in params.get("operations").keys():
params["operations"][op].update({
"parameters": rule_to_params(rule)
})
# Add extra parameters
if hasattr(view, "__apispec__"):
# Recursively update params
rupdate(params, view.__apispec__)
return params
def view2operations(view: View, spec: APISpec):
# Operations inherit tags from parent
inherited_tags = []
if hasattr(view, "__apispec__"):
inherited_tags = getattr(view, "__apispec__").get("tags", [])
# Build dictionary of operations (HTTP methods)
ops = {}
for method in View.methods:
if hasattr(view, method):
ops[method] = {}
rupdate(
ops[method],
{
"description": get_docstring(getattr(view, method)),
"summary": get_summary(getattr(view, method)),
"tags": inherited_tags,
},
)
rupdate(ops[method], method2operation(getattr(view, method), spec))
return ops
def method2operation(method: callable, spec: APISpec):
if hasattr(method, "__apispec__"):
apispec = getattr(method, "__apispec__")
else:
apispec = {}
op = {}
if "_params" in apispec:
rupdate(
op,
{
"requestBody": {
"content": {
"application/json": {
"schema": convert_schema(apispec.get("_params"), spec)
}
}
}
},
)
if "_schema" in apispec:
for code, schema in apispec.get("_schema", {}).items():
rupdate(
op,
{
"responses": {
code: {
"description": HTTPStatus(code).phrase,
"content": {
"application/json": {
"schema": convert_schema(schema, spec)
}
},
}
}
},
)
else:
# If no explicit responses are known, populate with defaults
rupdate(
op,
{
"responses": {
200: {"description": get_summary(method) or HTTPStatus(200).phrase}
}
},
)
# Bung in any extra swagger fields supplied
for key, val in apispec.items():
if not key in ["_params", "_schema"]:
rupdate(op, {key: val})
return op
def convert_schema(schema, spec: APISpec):
if isinstance(schema, BaseSchema):
return schema
elif isinstance(schema, Mapping):
return map2properties(schema, spec)
else:
raise TypeError(
"Unsupported schema type. Ensure schema is a Schema class, or dictionary of Field objects"
)
def map2properties(schema, spec: APISpec):
marshmallow_plugin = next(
plugin for plugin in spec.plugins if isinstance(plugin, MarshmallowPlugin)
)
converter = marshmallow_plugin.converter
d = {}
for k, v in schema.items():
if isinstance(v, Field):
d[k] = converter.field2property(v)
elif isinstance(v, Mapping):
d[k] = map2properties(v, spec)
else:
d[k] = v
return {"properties": d}

View file

@ -1,50 +0,0 @@
# -*- coding: utf-8 -*-
import re
import werkzeug.routing
PATH_RE = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
def rule_to_path(rule):
return PATH_RE.sub(r'{\1}', rule.rule)
# Conversion map of werkzeug rule converters to Javascript schema types
CONVERTER_MAPPING = {
werkzeug.routing.UnicodeConverter: ('string', None),
werkzeug.routing.IntegerConverter: ('integer', 'int32'),
werkzeug.routing.FloatConverter: ('number', 'float'),
}
DEFAULT_TYPE = ('string', None)
def rule_to_params(rule, overrides=None):
overrides = (overrides or {})
result = [
argument_to_param(argument, rule, overrides.get(argument, {}))
for argument in rule.arguments
]
for key in overrides.keys():
if overrides[key].get('in') in ('header', 'query'):
overrides[key]['name'] = overrides[key].get('name', key)
result.append(overrides[key])
return result
def argument_to_param(argument, rule, override=None):
param = {
'in': 'path',
'name': argument,
'required': True,
}
type_, format_ = CONVERTER_MAPPING.get(type(rule._converters[argument]), DEFAULT_TYPE)
param['type'] = type_
if format_ is not None:
param['format'] = format_
if rule.defaults and argument in rule.defaults:
param['default'] = rule.defaults[argument]
param.update(override or {})
return param

View file

@ -1,66 +0,0 @@
# Marshmallow fields to JSON schema types
# Note: We shouldn't ever need to use this directly. We should go via the apispec converter
from apispec.ext.marshmallow.field_converter import DEFAULT_FIELD_MAPPING
from . import fields
# Extra standard library Python types
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from typing import Dict, List, Tuple, Union
from uuid import UUID
"""
TODO: Use this to convert arbitrary dictionary into its own schema, for W3C TD
First: Convert Python non-builtins to builtins using DEFAULT_BUILTIN_CONVERSIONS
Then match types of each element to Field using DEFAULT_TYPE_MAPPING
Finally convert Fields to JSON using converter (preferred due to extra metadata), or DEFAULT_FIELD_MAPPING
"""
# Python types to Marshmallow fields
DEFAULT_TYPE_MAPPING = {
bool: fields.Boolean,
date: fields.Date,
datetime: fields.DateTime,
Decimal: fields.Decimal,
float: fields.Float,
int: fields.Integer,
str: fields.String,
time: fields.Time,
timedelta: fields.TimeDelta,
UUID: fields.UUID,
dict: fields.Dict,
Dict: fields.Dict,
}
# Functions to handle conversion of common Python types into serialisable Python types
def ndarray_to_list(o):
return o.tolist()
def to_int(o):
return int(o)
def to_float(o):
return float(o)
def to_string(o):
return str(o)
# Map of Python type conversions
DEFAULT_BUILTIN_CONVERSIONS = {
"numpy.ndarray": ndarray_to_list,
"numpy.int": to_int,
"fractions.Fraction": to_float,
}
# TODO: Deserialiser with inverse defaults
# TODO: Option to switch to .npy serialisation/deserialisation (or look for a better common array format)
# Use with [x.__module__+"."+x.__name__ for x in inspect.getmro(type(POSSIBLE_MATCHER))]
# Resulting array will contain strings with the same format as keys in DEFAULT_BUILTIN_CONVERSIONS

View file

@ -1,29 +0,0 @@
from openflexure_microscope.common.labthings_core.utilities import (
get_docstring,
get_summary,
)
from .view import View
from flask import current_app
def description_from_view(view_class):
summary = get_summary(view_class)
methods = []
for method_key in View.methods:
if hasattr(view_class, method_key):
methods.append(method_key.upper())
# If no class summary was given, try using summaries from method functions
if not summary:
summary = get_summary(getattr(view_class, method_key))
d = {"methods": methods, "description": summary}
return d
def view_class_from_endpoint(endpoint: str):
return current_app.view_functions[endpoint].view_class

View file

@ -1,27 +0,0 @@
from flask.views import MethodView
class View(MethodView):
"""
A LabThing Resource class should make use of functions get(), put(), post(), and delete()
corresponding to HTTP methods.
These functions will allow for automated documentation generation
"""
methods = ["get", "post", "put", "delete"]
endpoint = None
def __init__(self, *args, **kwargs):
MethodView.__init__(self, *args, **kwargs)
def doc(self):
docs = {"operations": {}}
if hasattr(self, "__apispec__"):
docs.update(self.__apispec__)
for meth in View.methods:
if hasattr(self, meth) and hasattr(getattr(self, meth), "__apispec__"):
docs["operations"][meth] = {}
docs["operations"][meth] = getattr(self, meth).__apispec__
return docs

View file

@ -1,113 +0,0 @@
from flask import abort, url_for, jsonify, render_template, Blueprint, current_app, request
from openflexure_microscope.common.labthings_core.utilities import get_docstring
from ...view import View
from ...find import current_labthing
from ...spec import rule_to_path, rule_to_params
import os
class APISpecView(View):
"""
OpenAPI v3 documentation
"""
def get(self):
"""
OpenAPI v3 documentation
"""
return jsonify(current_labthing().spec.to_dict())
class SwaggerUIView(View):
"""
Swagger UI documentation
"""
def get(self):
return render_template("swagger-ui.html")
class W3CThingDescriptionView(View):
"""
W3C-style Thing Description
"""
def get(self):
base_url = request.host_url.rstrip('/')
props = {}
for key, prop in current_labthing().properties.items():
prop_rules = current_app.url_map._rules_by_endpoint.get(prop.endpoint)
prop_urls = [rule_to_path(rule) for rule in prop_rules]
props[key] = {}
props[key]["title"] = prop.__name__
# TODO: Get description from __apispec__ preferentially
props[key]["description"] = get_docstring(prop) or (
get_docstring(prop.get) if hasattr(prop, "get") else ""
)
props[key]["readOnly"] = not (
hasattr(prop, "post") or hasattr(prop, "put") or hasattr(prop, "delete")
)
props[key]["writeOnly"] = not hasattr(prop, "get")
props[key]["links"] = [
{"href": f"{base_url}{url}"} for url in prop_urls
]
props[key]["uriVariables"] = {}
for prop_rule in prop_rules:
params = rule_to_params(prop_rule)
params_dict = {}
for param in params:
params_dict.update({
param.get("name"): {
"type": param.get("type")
}
})
props[key]["uriVariables"].update(params_dict)
if not props[key]["uriVariables"]:
del props[key]["uriVariables"]
actions = {}
for key, action in current_labthing().actions.items():
action_rules = current_app.url_map._rules_by_endpoint.get(action.endpoint)
action_urls = [rule_to_path(rule) for rule in action_rules]
actions[key] = {}
actions[key]["title"] = action.__name__
# TODO: Get description from __apispec__ preferentially
actions[key]["description"] = get_docstring(action) or (
get_docstring(action.post) if hasattr(action, "post") else ""
)
actions[key]["links"] = [
{"href": f"{base_url}{url}"} for url in action_urls
]
td = {
"@context": "https://www.w3.org/2019/wot/td/v1",
"id": url_for("labthings_docs.w3c_td", _external=True),
"title": current_labthing().title,
"description": current_labthing().description,
"properties": props,
"actions": actions,
}
return jsonify(td)
docs_blueprint = Blueprint(
"labthings_docs", __name__, static_folder="./static", template_folder="./templates"
)
docs_blueprint.add_url_rule(
"/swagger", view_func=APISpecView.as_view("swagger_json")
)
docs_blueprint.add_url_rule(
"/swagger-ui", view_func=SwaggerUIView.as_view("swagger_ui")
)
docs_blueprint.add_url_rule(
"/td", view_func=W3CThingDescriptionView.as_view("w3c_td")
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

View file

@ -1,60 +0,0 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js"> </script>
<script src="./swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "https://petstore.swagger.io/v2/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

View file

@ -1,68 +0,0 @@
<!doctype html>
<html lang="en-US">
<title>Swagger UI: OAuth2 Redirect</title>
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}
isValid = qp.state === sentState
if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="icon" type="image/png" href="{{ url_for('labthings_docs.static', filename='favicon-32x32.png') }}"
sizes="32x32" />
<link rel="icon" type="image/png" href="{{ url_for('labthings_docs.static', filename='favicon-16x16.png') }}"
sizes="16x16" />
<link href="{{ url_for('labthings_docs.static', filename='swagger-ui.css') }}" rel="stylesheet" type="text/css" />
</head>
<body class="swagger-section">
<div id="message-bar" class="swagger-ui-wrap" data-sw-translate>&nbsp;</div>
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
<script src="{{ url_for('labthings_docs.static', filename='swagger-ui-bundle.js') }}" type="text/javascript"></script>
<script src="{{ url_for('labthings_docs.static', filename='swagger-ui-standalone-preset.js') }}"
type="text/javascript"></script>
<script type="text/javascript">
var ui = SwaggerUIBundle({
url: "{{ url_for('labthings_docs.swagger_json') }}",
dom_id: '#swagger-ui-container',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "BaseLayout"
})
window.ui = ui
</script>
</body>
</html>

View file

@ -1,28 +0,0 @@
"""
Top-level representation of attached and enabled Extensions
"""
from ..view import View
from ..find import registered_extensions
from ..schema import ExtensionSchema
from ..decorators import marshal_with, ThingProperty
@ThingProperty
class ExtensionList(View):
"""
List and basic documentation for all enabled Extensions
"""
@marshal_with(ExtensionSchema(many=True))
def get(self):
"""
Return the current Extension forms
Returns an array of present Extension forms (describing Extension user interfaces.)
Please note, this is *not* a list of all enabled Extensions, only those with associated
user interface forms.
A complete list of enabled Extensions can be found in the microscope state.
"""
return registered_extensions().values()

View file

@ -1,51 +0,0 @@
from flask import abort, url_for
from ..decorators import marshal_with, ThingProperty, Tag
from ..view import View
from ..schema import TaskSchema
from openflexure_microscope.common.labthings_core import tasks
@ThingProperty
@Tag("tasks")
class TaskList(View):
@marshal_with(TaskSchema(many=True))
def get(self):
"""
List of all session tasks
"""
return tasks.tasks()
@Tag(["properties", "tasks"])
class TaskView(View):
@marshal_with(TaskSchema())
def get(self, id):
"""
Show status of a session task
Includes progress and intermediate data.
"""
try:
task = tasks.dict()[id]
except KeyError:
return abort(404) # 404 Not Found
return task
@marshal_with(TaskSchema())
def delete(self, id):
"""
Terminate a running task.
If the task is finished, deletes its entry.
"""
try:
task = tasks.dict()[id]
except KeyError:
return abort(404) # 404 Not Found
task.terminate()
return task

View file

@ -1,76 +0,0 @@
from threading import RLock
from openflexure_microscope.exceptions import LockError
class StrictLock(object):
"""
Class that behaves like a Python RLock, but with stricter timeout conditions and custom exceptions.
Args:
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
Attributes:
_lock (:py:class:`threading.RLock`): Parent RLock object
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
"""
def __init__(self, timeout=1):
self._lock = RLock()
self.timeout = timeout
def locked(self):
return self._lock.locked()
def acquire(self, blocking=True):
return self._lock.acquire(blocking, timeout=self.timeout)
def __enter__(self):
result = self._lock.acquire(blocking=True, timeout=self.timeout)
if result:
return result
else:
raise LockError("ACQUIRE_ERROR", self)
def __exit__(self, *args):
self._lock.release()
def release(self):
self._lock.release()
class CompositeLock(object):
"""
Class that behaves like a :py:class:`openflexure_microscope.common.lock.StrictLock`,
but allows multiple locks to be acquired and released.
Args:
locks (list): List of parent RLock objects
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
Attributes:
locks (list): List of parent RLock objects
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
"""
def __init__(self, locks, timeout=1):
self.locks = locks
self.timeout = timeout
def acquire(self, blocking=True):
return (lock.acquire(blocking=blocking) for lock in self.locks)
def __enter__(self):
result = (lock.acquire(blocking=True) for lock in self.locks)
if all(result):
return result
else:
raise LockError("ACQUIRE_ERROR", self)
def __exit__(self, *args):
for lock in self.locks:
lock.release()
def release(self):
for lock in self.locks:
lock.release()

View file

@ -1,25 +0,0 @@
__all__ = [
"taskify",
"tasks",
"dict",
"states",
"current_task",
"update_task_progress",
"cleanup_tasks",
"remove_task",
"update_task_data",
"ThreadTerminationError",
]
from .pool import (
tasks,
dict,
states,
current_task,
update_task_progress,
cleanup_tasks,
remove_task,
update_task_data,
taskify,
)
from .thread import ThreadTerminationError

View file

@ -1,145 +0,0 @@
import threading
import logging
from functools import wraps
from .thread import TaskThread
from flask import copy_current_request_context
class TaskMaster:
def __init__(self, *args, **kwargs):
self._tasks = []
@property
def tasks(self):
"""
Returns:
list: List of TaskThread objects.
"""
return self._tasks
@property
def dict(self):
"""
Returns:
dict: Dictionary of TaskThread objects. Key is TaskThread ID.
"""
return {str(t.id): t for t in self._tasks}
@property
def states(self):
"""
Returns:
dict: Dictionary of TaskThread.state dictionaries. Key is TaskThread ID.
"""
return {str(t.id): t.state for t in self._tasks}
def new(self, f, *args, **kwargs):
# copy_current_request_context allows threads to access flask current_app
task = TaskThread(
target=copy_current_request_context(f), args=args, kwargs=kwargs
)
self._tasks.append(task)
return task
def remove(self, task_id):
for task in self._tasks:
if (task.id == task_id) and not task.isAlive():
del task
def cleanup(self):
for task in self._tasks:
if not task.isAlive():
del task
# Task management
def tasks():
"""
List of tasks in default taskmaster
Returns:
list: List of tasks in default taskmaster
"""
global _default_task_master
return _default_task_master.tasks
def dict():
"""
Dictionary of tasks in default taskmaster
Returns:
dict: Dictionary of tasks in default taskmaster
"""
global _default_task_master
return _default_task_master.dict
def states():
"""
Dictionary of TaskThread.state dictionaries. Key is TaskThread ID.
Returns:
dict: Dictionary of task states in default taskmaster
"""
global _default_task_master
return _default_task_master.states
def cleanup_tasks():
global _default_task_master
return _default_task_master.cleanup()
def remove_task(task_id: str):
global _default_task_master
return _default_task_master.remove(task_id)
# Operations on the current task
def current_task():
current_task_thread = threading.current_thread()
if not isinstance(current_task_thread, TaskThread):
return None
return current_task_thread
def update_task_progress(progress: int):
if current_task():
current_task().update_progress(progress)
else:
logging.info("Cannot update task progress of __main__ thread. Skipping.")
def update_task_data(data: dict):
if current_task():
current_task().update_data(data)
else:
logging.info("Cannot update task data of __main__ thread. Skipping.")
# Main "taskify" functions
def taskify(f):
"""
A decorator that wraps the passed in function
and surpresses exceptions should one occur
"""
@wraps(f)
def wrapped(*args, **kwargs):
task = _default_task_master.new(
f, *args, **kwargs
) # Append to parent object's task list
task.start() # Start the function
return task
return wrapped
# Create our default, protected, module-level task pool
_default_task_master = TaskMaster()

View file

@ -1,200 +0,0 @@
import ctypes
import datetime
import logging
import traceback
import uuid
import threading
_LOG = logging.getLogger(__name__)
class ThreadTerminationError(SystemExit):
"""Sibling of SystemExit, but specific to thread termination."""
class TaskThread(threading.Thread):
def __init__(self, target=None, name=None, args=(), kwargs={}, daemon=True):
threading.Thread.__init__(
self,
group=None,
target=target,
name=name,
args=args,
kwargs=kwargs,
daemon=daemon,
)
# A UUID for the TaskThread (not the same as the threading.Thread ident)
self._ID = uuid.uuid4() # Task ID
# Make _target, _args, and _kwargs available to the subclass
self._target = target
self._args = args
self._kwargs = kwargs
# Nice string representation of target function
self.target_string = f"{self._target}(args={self._args}, kwargs={self._kwargs})"
# Private state properties
self._status: str = "idle" # Task status
self._return_value = None # Return value
self._start_time = None # Task start time
self._end_time = None # Task end time
# Public state properties
self.progress: int = None # Percent progress of the task
self.data = {} # Dictionary of custom data added during the task
# Stuff for handling termination
self._running_lock = (
threading.Lock()
) # Lock obtained while self._target is running
self._killed = (
threading.Event()
) # Event triggered when thread is manually terminated
@property
def id(self):
"""
Return ID of current TaskThread
"""
return self._ID
@property
def state(self):
return {
"function": self.target_string,
"id": self._ID,
"status": self._status,
"progress": self.progress,
"data": self.data,
"return": self._return_value,
"start_time": self._start_time,
"end_time": self._end_time,
}
def update_progress(self, progress: int):
# Update progress of the task
self.progress = progress
def update_data(self, data: dict):
# Store data to be used before task finishes (eg for real-time plotting)
self.data.update(data)
def _thread_proc(self, f):
"""
Wraps the target function to handle recording `status` and `return` to `state`.
Happens inside the task thread.
"""
def wrapped(*args, **kwargs):
nonlocal self
self._status = "running"
self._start_time = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
try:
self._return_value = f(*args, **kwargs)
self._status = "success"
except Exception as e:
logging.error(e)
logging.error(traceback.format_exc())
self._return_value = str(e)
self._status = "error"
finally:
self._end_time = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
return wrapped
def run(self):
"""
Overrides default threading.Thread run() method
"""
logging.debug((self._args, self._kwargs))
try:
with self._running_lock:
if self._killed.is_set():
raise ThreadTerminationError()
if self._target:
self._thread_proc(self._target)(*self._args, **self._kwargs)
finally:
# Avoid a refcycle if the thread is running a function with
# an argument that has a member that points to the thread.
del self._target, self._args, self._kwargs
def wait(self):
"""
Start waiting for the task to finish before returning
"""
print("Joining thread {}".format(self))
self.join()
return self._return_value
def async_raise(self, exc_type):
"""Raise an exception in this thread."""
# Should only be called on a started thread, so raise otherwise.
assert self.ident is not None, "Only started threads have thread identifier"
# If the thread has died we don't want to raise an exception so log.
if not self.is_alive():
_LOG.debug(
"Not raising %s because thread %s (%s) is not alive",
exc_type,
self.name,
self.ident,
)
return
result = ctypes.pythonapi.PyThreadState_SetAsyncExc(
ctypes.c_long(self.ident), ctypes.py_object(exc_type)
)
if result == 0 and self.is_alive():
# Don't raise an exception an error unnecessarily if the thread is dead.
raise ValueError("Thread ID was invalid.", self.ident)
elif result > 1:
# Something bad happened, call with a NULL exception to undo.
ctypes.pythonapi.PyThreadState_SetAsyncExc(self.ident, None)
raise RuntimeError(
"Error: PyThreadState_SetAsyncExc %s %s (%s) %s"
% (exc_type, self.name, self.ident, result)
)
def _is_thread_proc_running(self):
"""
Test if thread funtion (_thread_proc) is running,
by attemtping to acquire the lock _thread_proc acquires at runtime.
Returns:
bool: If _thread_proc is currently running
"""
could_acquire = self._running_lock.acquire(0)
if could_acquire:
self._running_lock.release()
return False
return True
def terminate(self):
"""
Raise ThreadTerminatedException in the context of the given thread,
which should cause the thread to exit silently.
"""
_LOG.warning(f"Terminating thread {self}")
self._killed.set()
if not self.is_alive():
logging.debug("Cannot kill thread that is no longer running.")
return
if not self._is_thread_proc_running():
logging.debug(
"Thread's _thread_proc function is no longer running, "
"will not kill; letting thread exit gracefully."
)
return
self.async_raise(ThreadTerminationError)
# Wait for the thread for finish closing. If the threaded function has cleanup code in a try-except,
# this pause allows it to finish running before the main process can continue.
while self._is_thread_proc_running():
pass
# Set state to terminated
self._status = "terminated"
self.progress = None

View file

@ -1,71 +0,0 @@
import collections.abc
import re
import base64
import operator
from functools import reduce
def get_docstring(obj):
ds = obj.__doc__
if ds:
return ds.strip()
else:
return ""
def get_summary(obj):
return get_docstring(obj).partition("\n")[0].strip()
def rupdate(d, u):
for k, v in u.items():
# Merge lists if they're present in both objects
if isinstance(v, list):
if not k in d:
d[k] = []
if isinstance(d[k], list):
d[k].extend(v)
# Recursively merge dictionaries if the element is a dictionary
elif isinstance(v, collections.abc.Mapping):
if not k in d:
d[k] = {}
d[k] = rupdate(d.get(k, {}), v)
# If not a list or dictionary, overwrite old value with new value
else:
d[k] = v
return d
def get_by_path(root, items):
"""Access a nested object in root by item sequence."""
return reduce(operator.getitem, items, root)
def set_by_path(root, items, value):
"""Set a value in a nested object in root by item sequence."""
get_by_path(root, items[:-1])[items[-1]] = value
def create_from_path(items):
tree_dict = {}
for key in reversed(items):
tree_dict = {key: tree_dict}
return tree_dict
def bottom_level_name(obj):
return obj.__name__.split(".")[-1]
def camel_to_snake(name):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def camel_to_spine(name):
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower()
def snake_to_spine(name):
return name.replace("_", "-")

View file

@ -8,15 +8,13 @@ as well as some Flask imports to simplify API route development
from openflexure_microscope.api.utilities import JsonResponse
# Task management
from openflexure_microscope.common.labthings_core.tasks import (
from labthings.core.tasks import (
current_task,
update_task_progress,
update_task_data,
taskify,
)
# Exceptions
from openflexure_microscope.exceptions import TaskDeniedException
# Flask things
from flask import abort, escape, jsonify, Response, request

View file

@ -1,10 +1,6 @@
from threading import ThreadError
class TaskDeniedException(Exception):
pass
class LockError(ThreadError):
ERROR_CODES = {
"ACQUIRE_ERROR": "Unable to acquire. Lock in use by another thread.",

View file

@ -14,7 +14,7 @@ from openflexure_microscope.camera.mock import MockStreamer
from openflexure_microscope.utilities import serialise_array_b64
from openflexure_microscope.config import user_settings
from openflexure_microscope.common.labthings_core.lock import CompositeLock
from labthings.core.lock import CompositeLock
class Microscope:
@ -33,7 +33,7 @@ class Microscope:
self.stage = None #: Currently connected stage object
# Initialise with an empty composite lock
#: :py:class:`openflexure_microscope.common.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([])
# Apply settings loaded from file

View file

@ -1,12 +1,12 @@
import numpy as np
from abc import ABCMeta, abstractmethod
from openflexure_microscope.common.labthings_core.lock import StrictLock
from labthings.core.lock import StrictLock
class BaseStage(metaclass=ABCMeta):
"""
Attributes:
lock (:py:class:`openflexure_microscope.common.lock.StrictLock`): Strict lock controlling thread
lock (:py:class:`labthings.lock.StrictLock`): Strict lock controlling thread
access to camera hardware
"""

530
poetry.lock generated
View file

@ -14,6 +14,14 @@ optional = false
python-versions = ">=3.5"
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]]
category = "dev"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
@ -47,6 +55,12 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
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]]
category = "dev"
description = "Internationalization utilities"
@ -72,6 +86,9 @@ attrs = ">=17.4.0"
click = ">=6.5"
toml = ">=0.9.4"
[package.extras]
d = ["aiohttp (>=3.3.2)"]
[[package]]
category = "dev"
description = "Python package for providing Mozilla's CA Bundle."
@ -127,6 +144,11 @@ Werkzeug = ">=0.15"
click = ">=5.1"
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]]
category = "main"
description = "A Flask extension adding a decorator for CORS support"
@ -163,6 +185,12 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "4.3.21"
[package.extras]
pipfile = ["pipreqs", "requirementslib"]
pyproject = ["toml"]
requirements = ["pipreqs", "pip-api"]
xdg_home = ["appdirs (>=1.4.0)"]
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
@ -182,6 +210,28 @@ version = "2.10.3"
[package.dependencies]
MarkupSafe = ">=0.23"
[package.extras]
i18n = ["Babel (>=0.8)"]
[[package]]
category = "main"
description = ""
name = "labthings"
optional = false
python-versions = "^3.6"
version = "0.1.0"
[package.dependencies]
Flask = "^1.1.1"
apispec = "^3.2.0"
flask-cors = "^3.0.8"
marshmallow = "^3.3.0"
webargs = "^5.5.2"
[package.source]
reference = "2854c2ea80fee1053d51edbe34cde8bbdb0364bb"
type = "git"
url = "https://github.com/labthings/python-labthings.git"
[[package]]
category = "dev"
description = "A fast and thorough lazy object proxy."
@ -206,6 +256,12 @@ optional = false
python-versions = ">=3.5"
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]]
category = "dev"
description = "McCabe checker, plugin for flake8"
@ -242,11 +298,15 @@ optional = true
python-versions = "*"
version = "1.13.1b0"
[package.extras]
array = ["numpy"]
doc = ["sphinx"]
test = ["coverage", "pytest", "mock", "pillow", "numpy"]
[package.source]
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
type = "git"
url = "https://github.com/rwb27/picamera.git"
[[package]]
category = "main"
description = "Python Imaging Library (Fork)"
@ -323,6 +383,10 @@ chardet = ">=3.0.2,<3.1.0"
idna = ">=2.5,<2.9"
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]]
category = "dev"
description = "a python refactoring library..."
@ -355,8 +419,8 @@ category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "1.13.0"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.14.0"
[[package]]
category = "dev"
@ -393,6 +457,10 @@ sphinxcontrib-jsmath = "*"
sphinxcontrib-qthelp = "*"
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]]
category = "dev"
description = ""
@ -401,6 +469,9 @@ optional = false
python-versions = "*"
version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
@ -409,6 +480,9 @@ optional = false
python-versions = "*"
version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
@ -417,6 +491,9 @@ optional = false
python-versions = "*"
version = "1.0.2"
[package.extras]
test = ["pytest", "flake8", "mypy", "html5lib"]
[[package]]
category = "dev"
description = "Sphinx domain for documenting HTTP APIs"
@ -437,6 +514,9 @@ optional = false
python-versions = ">=3.5"
version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
@ -445,6 +525,9 @@ optional = false
python-versions = "*"
version = "1.0.2"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
@ -453,6 +536,9 @@ optional = false
python-versions = "*"
version = "1.1.3"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language"
@ -478,6 +564,11 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
version = "1.25.7"
[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]]
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."
@ -489,6 +580,13 @@ version = "5.5.2"
[package.dependencies]
marshmallow = ">=2.15.2"
[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]]
category = "main"
description = "The comprehensive WSGI web application library."
@ -497,6 +595,11 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.16.0"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
termcolor = ["termcolor"]
watchdog = ["watchdog"]
[[package]]
category = "dev"
description = "Module for decorators, wrappers and monkey patching."
@ -509,60 +612,373 @@ version = "1.11.2"
rpi = ["picamera", "RPi.GPIO"]
[metadata]
content-hash = "26681a870da7f987373db62329a0dee4b39f6be767ef86faf47815f5f35abf8b"
content-hash = "53c82daa835ef29f7aac8ac7d36048487a9d989662a58a2d05d9a70465fbc8c6"
python-versions = "^3.6"
[metadata.hashes]
alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"]
apispec = ["c1625ae910f699c9adb21928693a3245b9faab6843f1b1c94ab2d505549cd78a", "f2ecb3a6534cfb42cf69b5c1222d2c0e695aab5ec3d7edde31b7354670948f05"]
appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"]
astroid = ["71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"]
attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
babel = ["1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38", "d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"]
black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"]
certifi = ["017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"]
chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"]
click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"]
docutils = ["0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"]
flask = ["13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", "45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"]
flask-cors = ["72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", "f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"]
idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"]
imagesize = ["6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"]
isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"]
itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"]
jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"]
lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "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"]
marshmallow = ["0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e", "3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"]
mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
numpy = ["1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6", "17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e", "20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc", "2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc", "39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a", "56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa", "590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3", "70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121", "77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971", "9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26", "9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd", "ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480", "b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec", "b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77", "b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57", "c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07", "cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572", "d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73", "e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca", "e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474", "f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"]
packaging = ["aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb", "fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"]
[metadata.files]
alabaster = [
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
]
apispec = [
{file = "apispec-3.2.0-py2.py3-none-any.whl", hash = "sha256:f2ecb3a6534cfb42cf69b5c1222d2c0e695aab5ec3d7edde31b7354670948f05"},
{file = "apispec-3.2.0.tar.gz", hash = "sha256:c1625ae910f699c9adb21928693a3245b9faab6843f1b1c94ab2d505549cd78a"},
]
appdirs = [
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
{file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
]
astroid = [
{file = "astroid-2.3.3-py3-none-any.whl", hash = "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"},
{file = "astroid-2.3.3.tar.gz", hash = "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
]
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 = []
lazy-object-proxy = [
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"},
{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.0-py2.py3-none-any.whl", hash = "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb"},
{file = "packaging-20.0.tar.gz", hash = "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"},
]
picamera = []
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"]
pygments = ["2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", "98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"]
pylint = ["3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"]
pyparsing = ["4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", "c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"]
pyserial = ["6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", "e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"]
pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"]
pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"]
requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
rope = ["6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", "f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"]
"rpi.gpio" = ["a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"]
scipy = ["03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351", "09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e", "10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e", "1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba", "409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb", "4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340", "6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1", "826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7", "a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae", "a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06", "adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef", "b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785", "c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d", "c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a", "c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921", "db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"]
six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"]
snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"]
sphinx = ["298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159", "e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5"]
sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"]
sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"]
sphinxcontrib-htmlhelp = ["4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"]
sphinxcontrib-httpdomain = ["1fb5375007d70bf180cdd1c79e741082be7aa2d37ba99efe561e1c2e3f38191e", "ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"]
sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"]
sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"]
sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
typed-ast = ["0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"]
urllib3 = ["a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", "f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"]
webargs = ["3beca296598067cec24a0b6f91c0afcc19b6e3c4d84ab026b931669628bb47b4", "3f9dc15de183d356c9a0acc159c100ea0506c0c240c1e6f1d8b308c5fed4dbbd", "fa4ad3ad9b38bedd26c619264fdc50d7ae014b49186736bca851e5b5228f2a1b"]
werkzeug = ["7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", "e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"]
wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"]
pillow = [
{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"},
{file = "Pillow-5.4.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7"},
{file = "Pillow-5.4.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e"},
{file = "Pillow-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c"},
{file = "Pillow-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f"},
{file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c"},
{file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc"},
{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"},
{file = "Pillow-5.4.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8"},
{file = "Pillow-5.4.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055"},
{file = "Pillow-5.4.1-cp34-cp34m-win32.whl", hash = "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6"},
{file = "Pillow-5.4.1-cp34-cp34m-win_amd64.whl", hash = "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf"},
{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"},
{file = "Pillow-5.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3"},
{file = "Pillow-5.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697"},
{file = "Pillow-5.4.1-cp35-cp35m-win32.whl", hash = "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7"},
{file = "Pillow-5.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe"},
{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"},
{file = "Pillow-5.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"},
{file = "Pillow-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad"},
{file = "Pillow-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a"},
{file = "Pillow-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356"},
{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"},
{file = "Pillow-5.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9"},
{file = "Pillow-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3"},
{file = "Pillow-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f"},
{file = "Pillow-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3"},
{file = "Pillow-5.4.1-pp260-pypy_41-win32.whl", hash = "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494"},
{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.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"},
{file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"},
]
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

@ -28,15 +28,13 @@ Flask = "^1.0"
pyyaml = "^3.13"
numpy = "^1.17.0"
Pillow = "^5.4"
flask-cors = "^3.0"
scipy = "1.3.0" # Exact version until we can guarantee a wheel
picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch.
"RPi.GPIO" = { version = "^0.6.5", optional = true }
marshmallow = "^3.3" # LabThings
webargs = "^5.5.2" # LabThings
pyserial = "^3.4" # Used for sangaboard (basic_serial_instrument) until we move to sangaboard pip library
apispec = "^3.2" # LabThings
labthings = { git = "https://github.com/labthings/python-labthings.git", branch = "master"}
[tool.poetry.extras]
rpi = ["picamera", "RPi.GPIO"]