From fde6181c9bc75a5fae890a887aa70da09946b102 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Fri, 4 Sep 2020 17:19:17 +0100 Subject: [PATCH] Removed websocket support --- README.md | 317 ++- docs/basic_usage/app_thing_server.rst | 118 +- docs/basic_usage/ws_api_structure.rst | 4 - docs/plan.md | 126 +- docs/quickstart.rst | 554 +++-- examples/simple_thing.py | 251 +- poetry.lock | 2498 ++++++++++---------- pyproject.toml | 99 +- src/labthings/__init__.py | 125 +- src/labthings/default_views/sockets.py | 42 - src/labthings/labthing.py | 1043 ++++---- src/labthings/sockets.py | 18 - src/labthings/td.py | 519 ++-- src/labthings/views/__init__.py | 514 ++-- src/labthings/wsgi.py | 280 +-- tests/conftest.py | 609 ++--- tests/test_default_views_socket_handler.py | 16 - 17 files changed, 3423 insertions(+), 3710 deletions(-) delete mode 100644 docs/basic_usage/ws_api_structure.rst delete mode 100644 src/labthings/default_views/sockets.py delete mode 100644 src/labthings/sockets.py delete mode 100644 tests/test_default_views_socket_handler.py diff --git a/README.md b/README.md index 47512ba9..6e463a54 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,150 @@ -# Python LabThings (for Flask) - -[![LabThings](https://img.shields.io/badge/-LabThings-8E00FF?style=flat&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz4NCjxzdmcgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIyIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNjMgMTYzIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im0xMjIuMjQgMTYyLjk5aDQwLjc0OHYtMTYyLjk5aC0xMDEuODd2NDAuNzQ4aDYxLjEyMnYxMjIuMjR6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTAgMTIuMjI0di0xMi4yMjRoNDAuNzQ4djEyMi4yNGg2MS4xMjJ2NDAuNzQ4aC0xMDEuODd2LTEyLjIyNGgyMC4zNzR2LTguMTVoLTIwLjM3NHYtOC4xNDloOC4wMTl2LTguMTVoLTguMDE5di04LjE1aDIwLjM3NHYtOC4xNDloLTIwLjM3NHYtOC4xNWg4LjAxOXYtOC4xNWgtOC4wMTl2LTguMTQ5aDIwLjM3NHYtOC4xNWgtMjAuMzc0di04LjE0OWg4LjAxOXYtOC4xNWgtOC4wMTl2LTguMTVoMjAuMzc0di04LjE0OWgtMjAuMzc0di04LjE1aDguMDE5di04LjE0OWgtOC4wMTl2LTguMTVoMjAuMzc0di04LjE1aC0yMC4zNzR6IiBmaWxsPSIjZmZmIi8+PC9zdmc+DQo=)](https://github.com/labthings/) -[![ReadTheDocs](https://readthedocs.org/projects/python-labthings/badge/?version=latest&style=flat)](https://python-labthings.readthedocs.io/en/latest/) -[![PyPI](https://img.shields.io/pypi/v/labthings)](https://pypi.org/project/labthings/) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![codecov](https://codecov.io/gh/labthings/python-labthings/branch/master/graph/badge.svg)](https://codecov.io/gh/labthings/python-labthings) -[![Riot.im](https://img.shields.io/badge/chat-on%20riot.im-368BD6)](https://riot.im/app/#/room/#labthings:matrix.org) - -A thread-based Python implementation of the LabThings API structure, based on the Flask microframework. - -## Installation - -`pip install labthings` - -## Quickstart example - -This example assumes a `PretendSpectrometer` class, which already has `data` and `integration_time` attributes, as well as an `average_data(n)` method. LabThings allows you to easily convert this existing instrument control code into a fully documented, standardised web API complete with auto-discovery and automatic background task threading. - -```python -#!/usr/bin/env python -import time - -from labthings import ActionView, PropertyView, create_app, fields, find_component, op -from labthings.example_components import PretendSpectrometer -from labthings.json import encode_json - -""" -Class for our lab component functionality. This could include serial communication, -equipment API calls, network requests, or a "virtual" device as seen here. -""" - - -""" -Create a view to view and change our integration_time value, -and register is as a Thing property -""" - - -# Wrap in a semantic annotation to autmatically set schema and args -class DenoiseProperty(PropertyView): - """Value of integration_time""" - - schema = fields.Int(required=True, minimum=100, maximum=500) - semtype = "LevelProperty" - - @op.readproperty - def get(self): - # When a GET request is made, we'll find our attached component - my_component = find_component("org.labthings.example.mycomponent") - return my_component.integration_time - - @op.writeproperty - def put(self, new_property_value): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - - # Apply the new value - my_component.integration_time = new_property_value - - return my_component.integration_time - - @op.observeproperty - def websocket(self, ws): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - initial_value = None - while not ws.closed: - time.sleep(1) - if my_component.integration_time != initial_value: - ws.send(encode_json(my_component.integration_time)) - initial_value = my_component.integration_time - - -""" -Create a view to quickly get some noisy data, and register is as a Thing property -""" - - -class QuickDataProperty(PropertyView): - """Show the current data value""" - - # Marshal the response as a list of floats - schema = fields.List(fields.Float()) - - @op.readproperty - def get(self): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - return my_component.data - - @op.observeproperty - def websocket(self, ws): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - while not ws.closed: - ws.send(encode_json(my_component.data)) - - -""" -Create a view to start an averaged measurement, and register is as a Thing action -""" - - -class MeasurementAction(ActionView): - # Expect JSON parameters in the request body. - # Pass to post function as dictionary argument. - args = { - "averages": fields.Integer( - missing=20, example=20, description="Number of data sets to average over", - ) - } - # Marshal the response as a list of numbers - schema = fields.List(fields.Number) - - # Main function to handle POST requests - @op.invokeaction - def post(self, args): - """Start an averaged measurement""" - - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - - # Get arguments and start a background task - n_averages = args.get("averages") - - # Return the task information - return my_component.average_data(n_averages) - - -# Create LabThings Flask app -app, labthing = create_app( - __name__, - title="My Lab Device API", - description="Test LabThing-based API", - version="0.1.0", -) - -# Attach an instance of our component -# Usually a Python object controlling some piece of hardware -my_spectrometer = PretendSpectrometer() -labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") - - -# Add routes for the API views we created -labthing.add_view(DenoiseProperty, "/integration_time") -labthing.add_view(QuickDataProperty, "/quick-data") -labthing.add_view(MeasurementAction, "/actions/measure") - - -# Start the app -if __name__ == "__main__": - from labthings import Server - - Server(app).run() -``` - -## Acknowledgements - -Much of the code surrounding default response formatting has been liberally taken from [Flask-RESTful](https://github.com/flask-restful/flask-restful). The integrated [Marshmallow](https://github.com/marshmallow-code/marshmallow) support was inspired by [Flask-Marshmallow](https://github.com/marshmallow-code/flask-marshmallow) and [Flask-ApiSpec](https://github.com/jmcarp/flask-apispec). - -## Developer notes - -### Changelog generation - -* `npm install -g conventional-changelog-cli` -* `conventional-changelog -r 1 --config ./changelog.config.js -i CHANGELOG.md -s` +# Python LabThings (for Flask) + +[![LabThings](https://img.shields.io/badge/-LabThings-8E00FF?style=flat&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz4NCjxzdmcgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIyIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNjMgMTYzIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im0xMjIuMjQgMTYyLjk5aDQwLjc0OHYtMTYyLjk5aC0xMDEuODd2NDAuNzQ4aDYxLjEyMnYxMjIuMjR6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTAgMTIuMjI0di0xMi4yMjRoNDAuNzQ4djEyMi4yNGg2MS4xMjJ2NDAuNzQ4aC0xMDEuODd2LTEyLjIyNGgyMC4zNzR2LTguMTVoLTIwLjM3NHYtOC4xNDloOC4wMTl2LTguMTVoLTguMDE5di04LjE1aDIwLjM3NHYtOC4xNDloLTIwLjM3NHYtOC4xNWg4LjAxOXYtOC4xNWgtOC4wMTl2LTguMTQ5aDIwLjM3NHYtOC4xNWgtMjAuMzc0di04LjE0OWg4LjAxOXYtOC4xNWgtOC4wMTl2LTguMTVoMjAuMzc0di04LjE0OWgtMjAuMzc0di04LjE1aDguMDE5di04LjE0OWgtOC4wMTl2LTguMTVoMjAuMzc0di04LjE1aC0yMC4zNzR6IiBmaWxsPSIjZmZmIi8+PC9zdmc+DQo=)](https://github.com/labthings/) +[![ReadTheDocs](https://readthedocs.org/projects/python-labthings/badge/?version=latest&style=flat)](https://python-labthings.readthedocs.io/en/latest/) +[![PyPI](https://img.shields.io/pypi/v/labthings)](https://pypi.org/project/labthings/) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![codecov](https://codecov.io/gh/labthings/python-labthings/branch/master/graph/badge.svg)](https://codecov.io/gh/labthings/python-labthings) +[![Riot.im](https://img.shields.io/badge/chat-on%20riot.im-368BD6)](https://riot.im/app/#/room/#labthings:matrix.org) + +A thread-based Python implementation of the LabThings API structure, based on the Flask microframework. + +## Installation + +`pip install labthings` + +## Quickstart example + +This example assumes a `PretendSpectrometer` class, which already has `data` and `integration_time` attributes, as well as an `average_data(n)` method. LabThings allows you to easily convert this existing instrument control code into a fully documented, standardised web API complete with auto-discovery and automatic background task threading. + +```python +#!/usr/bin/env python +import time + +from labthings import ActionView, PropertyView, create_app, fields, find_component, op +from labthings.example_components import PretendSpectrometer +from labthings.json import encode_json + +""" +Class for our lab component functionality. This could include serial communication, +equipment API calls, network requests, or a "virtual" device as seen here. +""" + + +""" +Create a view to view and change our integration_time value, +and register is as a Thing property +""" + + +# Wrap in a semantic annotation to autmatically set schema and args +class DenoiseProperty(PropertyView): + """Value of integration_time""" + + schema = fields.Int(required=True, minimum=100, maximum=500) + semtype = "LevelProperty" + + @op.readproperty + def get(self): + # When a GET request is made, we'll find our attached component + my_component = find_component("org.labthings.example.mycomponent") + return my_component.integration_time + + @op.writeproperty + def put(self, new_property_value): + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + + # Apply the new value + my_component.integration_time = new_property_value + + return my_component.integration_time + + +""" +Create a view to quickly get some noisy data, and register is as a Thing property +""" + + +class QuickDataProperty(PropertyView): + """Show the current data value""" + + # Marshal the response as a list of floats + schema = fields.List(fields.Float()) + + @op.readproperty + def get(self): + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + return my_component.data + + + +""" +Create a view to start an averaged measurement, and register is as a Thing action +""" + + +class MeasurementAction(ActionView): + # Expect JSON parameters in the request body. + # Pass to post function as dictionary argument. + args = { + "averages": fields.Integer( + missing=20, example=20, description="Number of data sets to average over", + ) + } + # Marshal the response as a list of numbers + schema = fields.List(fields.Number) + + # Main function to handle POST requests + @op.invokeaction + def post(self, args): + """Start an averaged measurement""" + + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + + # Get arguments and start a background task + n_averages = args.get("averages") + + # Return the task information + return my_component.average_data(n_averages) + + +# Create LabThings Flask app +app, labthing = create_app( + __name__, + title="My Lab Device API", + description="Test LabThing-based API", + version="0.1.0", +) + +# Attach an instance of our component +# Usually a Python object controlling some piece of hardware +my_spectrometer = PretendSpectrometer() +labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") + + +# Add routes for the API views we created +labthing.add_view(DenoiseProperty, "/integration_time") +labthing.add_view(QuickDataProperty, "/quick-data") +labthing.add_view(MeasurementAction, "/actions/measure") + + +# Start the app +if __name__ == "__main__": + from labthings import Server + + Server(app).run() +``` + +## Acknowledgements + +Much of the code surrounding default response formatting has been liberally taken from [Flask-RESTful](https://github.com/flask-restful/flask-restful). The integrated [Marshmallow](https://github.com/marshmallow-code/marshmallow) support was inspired by [Flask-Marshmallow](https://github.com/marshmallow-code/flask-marshmallow) and [Flask-ApiSpec](https://github.com/jmcarp/flask-apispec). + +## Developer notes + +### Changelog generation + +* `npm install -g conventional-changelog-cli` +* `conventional-changelog -r 1 --config ./changelog.config.js -i CHANGELOG.md -s` diff --git a/docs/basic_usage/app_thing_server.rst b/docs/basic_usage/app_thing_server.rst index 57245696..b5282caf 100644 --- a/docs/basic_usage/app_thing_server.rst +++ b/docs/basic_usage/app_thing_server.rst @@ -1,60 +1,60 @@ -App, LabThing, and Server -========================= - -Python LabThings works as a Flask extension, and so we introduce two key objects: the :class:`flask.Flask` app, and the :class:`labthings.LabThing` object. The :class:`labthings.LabThing` object is our main entrypoint for the Flask application, and all LabThings functionality is added via this object. - -In order to enable threaded actions, and concurrent WebSocket connections, the app should be served using the :class:`labthings.Server` class. Other production servers such as Gevent can be used, however this will require monkey-patching and has not been comprehensively tested. - - -Create app ----------- - -The :meth:`labthings.create_app` function automatically creates a Flask app object, enables up cross-origin resource sharing, and initialises a :class:`labthings.LabThing` instance on the app. The function returns both in a tuple. - -.. autofunction:: labthings.create_app - :noindex: - -ALternatively, the app and :class:`labthings.LabThing` objects can be initialised and attached separately, for example: - -.. code-block:: python - - from flask import Flask - from labthings import LabThing - - app = Flask(__name__) - labthing = LabThing(app) - - -LabThing --------- - -The LabThing object is our main entrypoint, and handles creating API views, managing background actions, tracking logs, and generating API documentation. - -.. autoclass:: labthings.LabThing - :noindex: - - -Views ------ - -Thing interaction affordances are created using Views. Two main View types correspond to properties and actions. - -.. autoclass:: labthings.PropertyView - :noindex: - -.. autoclass:: labthings.ActionView - :noindex: - - -Server ------- - -The integrated server actually handles 3 distinct server functions: WSGI HTTP requests, routing WebSocket requests to the threaded handler, and registering mDNS records for automatic Thing discovery. It is therefore strongly suggested you use the builtin server. - -**Important notes:** - -The integrated server will spawn a new native thread *per-connection*. This will only function well in situations where few (<50) simultaneous connections are expected, such as local Web of Things devices. Do not use this server in any public web app where many connections are expected. It is designed exclusively with low-traffic LAN access in mind. - -.. autoclass:: labthings.Server - :members: +App, LabThing, and Server +========================= + +Python LabThings works as a Flask extension, and so we introduce two key objects: the :class:`flask.Flask` app, and the :class:`labthings.LabThing` object. The :class:`labthings.LabThing` object is our main entrypoint for the Flask application, and all LabThings functionality is added via this object. + +In order to enable threaded actions the app should be served using the :class:`labthings.Server` class. Other production servers such as Gevent can be used, however this will require monkey-patching and has not been comprehensively tested. + + +Create app +---------- + +The :meth:`labthings.create_app` function automatically creates a Flask app object, enables up cross-origin resource sharing, and initialises a :class:`labthings.LabThing` instance on the app. The function returns both in a tuple. + +.. autofunction:: labthings.create_app + :noindex: + +ALternatively, the app and :class:`labthings.LabThing` objects can be initialised and attached separately, for example: + +.. code-block:: python + + from flask import Flask + from labthings import LabThing + + app = Flask(__name__) + labthing = LabThing(app) + + +LabThing +-------- + +The LabThing object is our main entrypoint, and handles creating API views, managing background actions, tracking logs, and generating API documentation. + +.. autoclass:: labthings.LabThing + :noindex: + + +Views +----- + +Thing interaction affordances are created using Views. Two main View types correspond to properties and actions. + +.. autoclass:: labthings.PropertyView + :noindex: + +.. autoclass:: labthings.ActionView + :noindex: + + +Server +------ + +The integrated server actually handles 3 distinct server functions: WSGI HTTP requests, and registering mDNS records for automatic Thing discovery. It is therefore strongly suggested you use the builtin server. + +**Important notes:** + +The integrated server will spawn a new native thread *per-connection*. This will only function well in situations where few (<50) simultaneous connections are expected, such as local Web of Things devices. Do not use this server in any public web app where many connections are expected. It is designed exclusively with low-traffic LAN access in mind. + +.. autoclass:: labthings.Server + :members: :noindex: \ No newline at end of file diff --git a/docs/basic_usage/ws_api_structure.rst b/docs/basic_usage/ws_api_structure.rst deleted file mode 100644 index b71a7890..00000000 --- a/docs/basic_usage/ws_api_structure.rst +++ /dev/null @@ -1,4 +0,0 @@ -WebSocket API Structure -======================= - -*Documentation to be written* \ No newline at end of file diff --git a/docs/plan.md b/docs/plan.md index 5f81e0b0..58cc9642 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,66 +1,62 @@ -# Documentation plan/structure - -## Quickstart - -## App, LabThing, and Server - -* create_app -* LabThing class -* Server class -* current_labthing - -### HTTP API structure - -* Thing Description (root) -* Swagger-UI -* Action queue -* Extension list - -### WebSocket API structure - -* Not yet implemented - -### Serialising data - -* fields -* schema -* semantics - -### Action tasks - -* Preamble (all actions are tasks, etc) -* Labthing.actions TaskPool -* current_task -* update_task_progress -* update_task_data -* Stopping tasks - * TaskThread.stopped event - * TaskKillException - -### Synchronisation - -* StrictLock -* CompositeLock -* ClientEvent - -## Advanced usage - -### View classes - -* labthings.views - -### Components - -* Access to Python objects by name - * Used to access hardware from within Views -* registered_components, find_component - -### Encoders - -* labthings.json.LabThingsJSONEncoder - -### Extensions - -* labthings.extensions.BaseExtension -* labthings.extensions.find_extensions +# Documentation plan/structure + +## Quickstart + +## App, LabThing, and Server + +* create_app +* LabThing class +* Server class +* current_labthing + +### HTTP API structure + +* Thing Description (root) +* Swagger-UI +* Action queue +* Extension list + +### Serialising data + +* fields +* schema +* semantics + +### Action tasks + +* Preamble (all actions are tasks, etc) +* Labthing.actions TaskPool +* current_task +* update_task_progress +* update_task_data +* Stopping tasks + * TaskThread.stopped event + * TaskKillException + +### Synchronisation + +* StrictLock +* CompositeLock +* ClientEvent + +## Advanced usage + +### View classes + +* labthings.views + +### Components + +* Access to Python objects by name + * Used to access hardware from within Views +* registered_components, find_component + +### Encoders + +* labthings.json.LabThingsJSONEncoder + +### Extensions + +* labthings.extensions.BaseExtension +* labthings.extensions.find_extensions * registered_extensions, find_extension \ No newline at end of file diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f101e592..78251e07 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,286 +1,268 @@ -Quickstart -========== - -The easiest way to get started with Python-LabThings is via the :meth:`labthings.create_app` function, and the :class:`labthings.LabThing` builder methods. - -We will assume that for basic usage you already have some basic instrument control code. In our example, this is in the form of a ``PretendSpectrometer`` class, which will generate some data like your instrument control code might. Our ``PretendSpectrometer`` class has a ``data`` attribute which quickly returns a spectrum, an ``x_range`` attribute which determines the range of data we'll return, an ``integration_time`` attribute for cleaning up our signal, and a slow ``average_data(n)`` method to average ``n`` individual data measurements. - -Building an API from this class requires a few extra considerations. In order to tell our API what data to expect from users, we need to construct a schema for each of our interactions. This schema simply maps variable names to JSON-compatible types, and is made simple via the :mod:`labthings.fields` module. - -For properties, the input and output MUST be formatted the same, and so a single ``schema`` argument handles both. For actions, the input parameters and output response may be different. In this case, we can pass a ``schema`` argument to format the output, and an ``args`` argument to specify the input parameters, - -An example Lab Thing built from our ``PretendSpectrometer`` class, complete with schemas, might look like: - - -.. code-block:: python - - import time - - from labthings import ActionView, PropertyView, create_app, fields, find_component, op - from labthings.example_components import PretendSpectrometer - from labthings.json import encode_json - - """ - Class for our lab component functionality. This could include serial communication, - equipment API calls, network requests, or a "virtual" device as seen here. - """ - - - """ - Create a view to view and change our integration_time value, - and register is as a Thing property - """ - - - # Wrap in a semantic annotation to autmatically set schema and args - class DenoiseProperty(PropertyView): - """Value of integration_time""" - - schema = fields.Int(required=True, minimum=100, maximum=500) - semtype = "LevelProperty" - - @op.readproperty - def get(self): - # When a GET request is made, we'll find our attached component - my_component = find_component("org.labthings.example.mycomponent") - return my_component.integration_time - - @op.writeproperty - def put(self, new_property_value): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - - # Apply the new value - my_component.integration_time = new_property_value - - return my_component.integration_time - - @op.observeproperty - def websocket(self, ws): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - initial_value = None - while not ws.closed: - time.sleep(1) - if my_component.integration_time != initial_value: - ws.send(encode_json(my_component.integration_time)) - initial_value = my_component.integration_time - - - """ - Create a view to quickly get some noisy data, and register is as a Thing property - """ - - - class QuickDataProperty(PropertyView): - """Show the current data value""" - - # Marshal the response as a list of floats - schema = fields.List(fields.Float()) - - @op.readproperty - def get(self): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - return my_component.data - - @op.observeproperty - def websocket(self, ws): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - while not ws.closed: - ws.send(encode_json(my_component.data)) - - - """ - Create a view to start an averaged measurement, and register is as a Thing action - """ - - - class MeasurementAction(ActionView): - # Expect JSON parameters in the request body. - # Pass to post function as dictionary argument. - args = { - "averages": fields.Integer( - missing=20, example=20, description="Number of data sets to average over", - ) - } - # Marshal the response as a list of numbers - schema = fields.List(fields.Number) - - # Main function to handle POST requests - @op.invokeaction - def post(self, args): - """Start an averaged measurement""" - - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - - # Get arguments and start a background task - n_averages = args.get("averages") - - # Return the task information - return my_component.average_data(n_averages) - - - # Create LabThings Flask app - app, labthing = create_app( - __name__, - title="My Lab Device API", - description="Test LabThing-based API", - version="0.1.0", - ) - - # Attach an instance of our component - # Usually a Python object controlling some piece of hardware - my_spectrometer = PretendSpectrometer() - labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") - - - # Add routes for the API views we created - labthing.add_view(DenoiseProperty, "/integration_time") - labthing.add_view(QuickDataProperty, "/quick-data") - labthing.add_view(MeasurementAction, "/actions/measure") - - - # Start the app - if __name__ == "__main__": - from labthings import Server - - Server(app).run() - - -Once started, the app will build and serve a full web API, and generate the following Thing Description: - -.. code-block:: json - - { - "@context": [ - "https://www.w3.org/2019/wot/td/v1", - "https://iot.mozilla.org/schemas/" - ], - "id": "http://127.0.0.1:7486/", - "base": "http://127.0.0.1:7486/", - "title": "My PretendSpectrometer API", - "description": "LabThing API for PretendSpectrometer", - "properties": { - "pretendSpectrometerData": { - "title": "PretendSpectrometer_data", - "description": "A single-shot measurement", - "readOnly": true, - "links": [{ - "href": "/properties/PretendSpectrometer/data" - }], - "forms": [{ - "op": "readproperty", - "htv:methodName": "GET", - "href": "/properties/PretendSpectrometer/data", - "contentType": "application/json" - }], - "type": "array", - "items": { - "type": "number", - "format": "decimal" - } - }, - "pretendSpectrometerMagicDenoise": { - "title": "PretendSpectrometer_magic_denoise", - "description": "Single-shot integration time", - "links": [{ - "href": "/properties/PretendSpectrometer/magic_denoise" - }], - "forms": [{ - "op": "readproperty", - "htv:methodName": "GET", - "href": "/properties/PretendSpectrometer/magic_denoise", - "contentType": "application/json" - }, - { - "op": "writeproperty", - "htv:methodName": "PUT", - "href": "/properties/PretendSpectrometer/magic_denoise", - "contentType": "application/json" - } - ], - "type": "number", - "format": "integer", - "min": 100, - "max": 500, - "example": 200 - } - }, - "actions": { - "averageDataAction": { - "title": "average_data_action", - "description": "Take an averaged measurement", - "links": [{ - "href": "/actions/PretendSpectrometer/average_data" - }], - "forms": [{ - "op": "invokeaction", - "htv:methodName": "POST", - "href": "/actions/PretendSpectrometer/average_data", - "contentType": "application/json" - }], - "input": { - "type": "object", - "properties": { - "n": { - "type": "number", - "format": "integer", - "default": 5, - "description": "Number of averages to take", - "example": 5 - } - } - } - } - }, - "links": [], - "securityDefinitions": {}, - "security": "nosec_sc" - } - - -For completeness of the examples, our ``PretendSpectrometer`` class code is: - -.. code-block:: python - - import random - import math - import time - - class PretendSpectrometer: - def __init__(self): - self.x_range = range(-100, 100) - self.integration_time = 200 - - def make_spectrum(self, x, mu=0.0, sigma=25.0): - """ - Generate a noisy gaussian function (to act as some pretend data) - - Our noise is inversely proportional to self.integration_time - """ - x = float(x - mu) / sigma - return ( - math.exp(-x * x / 2.0) / math.sqrt(2.0 * math.pi) / sigma - + (1 / self.integration_time) * random.random() - ) - - @property - def data(self): - """Return a 1D data trace.""" - time.sleep(self.integration_time / 1000) - return [self.make_spectrum(x) for x in self.x_range] - - def average_data(self, n: int): - """Average n-sets of data. Emulates a measurement that may take a while.""" - summed_data = self.data - - for _ in range(n): - summed_data = [summed_data[i] + el for i, el in enumerate(self.data)] - time.sleep(0.25) - - summed_data = [i / n for i in summed_data] - - return summed_data +Quickstart +========== + +The easiest way to get started with Python-LabThings is via the :meth:`labthings.create_app` function, and the :class:`labthings.LabThing` builder methods. + +We will assume that for basic usage you already have some basic instrument control code. In our example, this is in the form of a ``PretendSpectrometer`` class, which will generate some data like your instrument control code might. Our ``PretendSpectrometer`` class has a ``data`` attribute which quickly returns a spectrum, an ``x_range`` attribute which determines the range of data we'll return, an ``integration_time`` attribute for cleaning up our signal, and a slow ``average_data(n)`` method to average ``n`` individual data measurements. + +Building an API from this class requires a few extra considerations. In order to tell our API what data to expect from users, we need to construct a schema for each of our interactions. This schema simply maps variable names to JSON-compatible types, and is made simple via the :mod:`labthings.fields` module. + +For properties, the input and output MUST be formatted the same, and so a single ``schema`` argument handles both. For actions, the input parameters and output response may be different. In this case, we can pass a ``schema`` argument to format the output, and an ``args`` argument to specify the input parameters, + +An example Lab Thing built from our ``PretendSpectrometer`` class, complete with schemas, might look like: + + +.. code-block:: python + + import time + + from labthings import ActionView, PropertyView, create_app, fields, find_component, op + from labthings.example_components import PretendSpectrometer + from labthings.json import encode_json + + """ + Class for our lab component functionality. This could include serial communication, + equipment API calls, network requests, or a "virtual" device as seen here. + """ + + + """ + Create a view to view and change our integration_time value, + and register is as a Thing property + """ + + + # Wrap in a semantic annotation to autmatically set schema and args + class DenoiseProperty(PropertyView): + """Value of integration_time""" + + schema = fields.Int(required=True, minimum=100, maximum=500) + semtype = "LevelProperty" + + @op.readproperty + def get(self): + # When a GET request is made, we'll find our attached component + my_component = find_component("org.labthings.example.mycomponent") + return my_component.integration_time + + @op.writeproperty + def put(self, new_property_value): + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + + # Apply the new value + my_component.integration_time = new_property_value + + return my_component.integration_time + + + """ + Create a view to quickly get some noisy data, and register is as a Thing property + """ + + + class QuickDataProperty(PropertyView): + """Show the current data value""" + + # Marshal the response as a list of floats + schema = fields.List(fields.Float()) + + @op.readproperty + def get(self): + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + return my_component.data + + + """ + Create a view to start an averaged measurement, and register is as a Thing action + """ + + + class MeasurementAction(ActionView): + # Expect JSON parameters in the request body. + # Pass to post function as dictionary argument. + args = { + "averages": fields.Integer( + missing=20, example=20, description="Number of data sets to average over", + ) + } + # Marshal the response as a list of numbers + schema = fields.List(fields.Number) + + # Main function to handle POST requests + @op.invokeaction + def post(self, args): + """Start an averaged measurement""" + + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + + # Get arguments and start a background task + n_averages = args.get("averages") + + # Return the task information + return my_component.average_data(n_averages) + + + # Create LabThings Flask app + app, labthing = create_app( + __name__, + title="My Lab Device API", + description="Test LabThing-based API", + version="0.1.0", + ) + + # Attach an instance of our component + # Usually a Python object controlling some piece of hardware + my_spectrometer = PretendSpectrometer() + labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") + + + # Add routes for the API views we created + labthing.add_view(DenoiseProperty, "/integration_time") + labthing.add_view(QuickDataProperty, "/quick-data") + labthing.add_view(MeasurementAction, "/actions/measure") + + + # Start the app + if __name__ == "__main__": + from labthings import Server + + Server(app).run() + + +Once started, the app will build and serve a full web API, and generate the following Thing Description: + +.. code-block:: json + + { + "@context": [ + "https://www.w3.org/2019/wot/td/v1", + "https://iot.mozilla.org/schemas/" + ], + "id": "http://127.0.0.1:7486/", + "base": "http://127.0.0.1:7486/", + "title": "My PretendSpectrometer API", + "description": "LabThing API for PretendSpectrometer", + "properties": { + "pretendSpectrometerData": { + "title": "PretendSpectrometer_data", + "description": "A single-shot measurement", + "readOnly": true, + "links": [{ + "href": "/properties/PretendSpectrometer/data" + }], + "forms": [{ + "op": "readproperty", + "htv:methodName": "GET", + "href": "/properties/PretendSpectrometer/data", + "contentType": "application/json" + }], + "type": "array", + "items": { + "type": "number", + "format": "decimal" + } + }, + "pretendSpectrometerMagicDenoise": { + "title": "PretendSpectrometer_magic_denoise", + "description": "Single-shot integration time", + "links": [{ + "href": "/properties/PretendSpectrometer/magic_denoise" + }], + "forms": [{ + "op": "readproperty", + "htv:methodName": "GET", + "href": "/properties/PretendSpectrometer/magic_denoise", + "contentType": "application/json" + }, + { + "op": "writeproperty", + "htv:methodName": "PUT", + "href": "/properties/PretendSpectrometer/magic_denoise", + "contentType": "application/json" + } + ], + "type": "number", + "format": "integer", + "min": 100, + "max": 500, + "example": 200 + } + }, + "actions": { + "averageDataAction": { + "title": "average_data_action", + "description": "Take an averaged measurement", + "links": [{ + "href": "/actions/PretendSpectrometer/average_data" + }], + "forms": [{ + "op": "invokeaction", + "htv:methodName": "POST", + "href": "/actions/PretendSpectrometer/average_data", + "contentType": "application/json" + }], + "input": { + "type": "object", + "properties": { + "n": { + "type": "number", + "format": "integer", + "default": 5, + "description": "Number of averages to take", + "example": 5 + } + } + } + } + }, + "links": [], + "securityDefinitions": {}, + "security": "nosec_sc" + } + + +For completeness of the examples, our ``PretendSpectrometer`` class code is: + +.. code-block:: python + + import random + import math + import time + + class PretendSpectrometer: + def __init__(self): + self.x_range = range(-100, 100) + self.integration_time = 200 + + def make_spectrum(self, x, mu=0.0, sigma=25.0): + """ + Generate a noisy gaussian function (to act as some pretend data) + + Our noise is inversely proportional to self.integration_time + """ + x = float(x - mu) / sigma + return ( + math.exp(-x * x / 2.0) / math.sqrt(2.0 * math.pi) / sigma + + (1 / self.integration_time) * random.random() + ) + + @property + def data(self): + """Return a 1D data trace.""" + time.sleep(self.integration_time / 1000) + return [self.make_spectrum(x) for x in self.x_range] + + def average_data(self, n: int): + """Average n-sets of data. Emulates a measurement that may take a while.""" + summed_data = self.data + + for _ in range(n): + summed_data = [summed_data[i] + el for i, el in enumerate(self.data)] + time.sleep(0.25) + + summed_data = [i / n for i in summed_data] + + return summed_data diff --git a/examples/simple_thing.py b/examples/simple_thing.py index 03d8ed9e..1d7423ee 100644 --- a/examples/simple_thing.py +++ b/examples/simple_thing.py @@ -1,135 +1,116 @@ -#!/usr/bin/env python -import time - -from labthings import ActionView, PropertyView, create_app, fields, find_component, op -from labthings.example_components import PretendSpectrometer -from labthings.json import encode_json - -""" -Class for our lab component functionality. This could include serial communication, -equipment API calls, network requests, or a "virtual" device as seen here. -""" - - -""" -Create a view to view and change our integration_time value, -and register is as a Thing property -""" - - -# Wrap in a semantic annotation to autmatically set schema and args -class DenoiseProperty(PropertyView): - """Value of integration_time""" - - schema = fields.Int(required=True, minimum=100, maximum=500) - semtype = "LevelProperty" - - @op.readproperty - def get(self): - # When a GET request is made, we'll find our attached component - my_component = find_component("org.labthings.example.mycomponent") - return my_component.integration_time - - @op.writeproperty - def put(self, new_property_value): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - - # Apply the new value - my_component.integration_time = new_property_value - - return my_component.integration_time - - @op.observeproperty - def websocket(self, ws): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - initial_value = None - while not ws.closed: - time.sleep(1) - if my_component.integration_time != initial_value: - ws.send(encode_json(my_component.integration_time)) - initial_value = my_component.integration_time - - -""" -Create a view to quickly get some noisy data, and register is as a Thing property -""" - - -class QuickDataProperty(PropertyView): - """Show the current data value""" - - # Marshal the response as a list of floats - schema = fields.List(fields.Float()) - - @op.readproperty - def get(self): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - return my_component.data - - @op.observeproperty - def websocket(self, ws): - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - while not ws.closed: - ws.send(encode_json(my_component.data)) - - -""" -Create a view to start an averaged measurement, and register is as a Thing action -""" - - -class MeasurementAction(ActionView): - # Expect JSON parameters in the request body. - # Pass to post function as dictionary argument. - args = { - "averages": fields.Integer( - missing=20, example=20, description="Number of data sets to average over", - ) - } - # Marshal the response as a list of numbers - schema = fields.List(fields.Number) - - # Main function to handle POST requests - @op.invokeaction - def post(self, args): - """Start an averaged measurement""" - - # Find our attached component - my_component = find_component("org.labthings.example.mycomponent") - - # Get arguments and start a background task - n_averages = args.get("averages") - - # Return the task information - return my_component.average_data(n_averages) - - -# Create LabThings Flask app -app, labthing = create_app( - __name__, - title="My Lab Device API", - description="Test LabThing-based API", - version="0.1.0", -) - -# Attach an instance of our component -# Usually a Python object controlling some piece of hardware -my_spectrometer = PretendSpectrometer() -labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") - - -# Add routes for the API views we created -labthing.add_view(DenoiseProperty, "/integration_time") -labthing.add_view(QuickDataProperty, "/quick-data") -labthing.add_view(MeasurementAction, "/actions/measure") - - -# Start the app -if __name__ == "__main__": - from labthings import Server - - Server(app).run() +#!/usr/bin/env python +import time + +from labthings import ActionView, PropertyView, create_app, fields, find_component, op +from labthings.example_components import PretendSpectrometer +from labthings.json import encode_json + +""" +Class for our lab component functionality. This could include serial communication, +equipment API calls, network requests, or a "virtual" device as seen here. +""" + + +""" +Create a view to view and change our integration_time value, +and register is as a Thing property +""" + + +# Wrap in a semantic annotation to autmatically set schema and args +class DenoiseProperty(PropertyView): + """Value of integration_time""" + + schema = fields.Int(required=True, minimum=100, maximum=500) + semtype = "LevelProperty" + + @op.readproperty + def get(self): + # When a GET request is made, we'll find our attached component + my_component = find_component("org.labthings.example.mycomponent") + return my_component.integration_time + + @op.writeproperty + def put(self, new_property_value): + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + + # Apply the new value + my_component.integration_time = new_property_value + + return my_component.integration_time + + +""" +Create a view to quickly get some noisy data, and register is as a Thing property +""" + + +class QuickDataProperty(PropertyView): + """Show the current data value""" + + # Marshal the response as a list of floats + schema = fields.List(fields.Float()) + + @op.readproperty + def get(self): + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + return my_component.data + +""" +Create a view to start an averaged measurement, and register is as a Thing action +""" + + +class MeasurementAction(ActionView): + # Expect JSON parameters in the request body. + # Pass to post function as dictionary argument. + args = { + "averages": fields.Integer( + missing=20, example=20, description="Number of data sets to average over", + ) + } + # Marshal the response as a list of numbers + schema = fields.List(fields.Number) + + # Main function to handle POST requests + @op.invokeaction + def post(self, args): + """Start an averaged measurement""" + + # Find our attached component + my_component = find_component("org.labthings.example.mycomponent") + + # Get arguments and start a background task + n_averages = args.get("averages") + + # Return the task information + return my_component.average_data(n_averages) + + +# Create LabThings Flask app +app, labthing = create_app( + __name__, + title="My Lab Device API", + description="Test LabThing-based API", + version="0.1.0", +) + +# Attach an instance of our component +# Usually a Python object controlling some piece of hardware +my_spectrometer = PretendSpectrometer() +labthing.add_component(my_spectrometer, "org.labthings.example.mycomponent") + + +# Add routes for the API views we created +labthing.add_view(DenoiseProperty, "/integration_time") +labthing.add_view(QuickDataProperty, "/quick-data") +labthing.add_view(MeasurementAction, "/actions/measure") + + +# Start the app +if __name__ == "__main__": + from labthings import Server + + Server(app).run() diff --git a/poetry.lock b/poetry.lock index f96328c5..5b40dc76 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,1249 +1,1249 @@ -[[package]] -category = "dev" -description = "A configurable sidebar-enabled Sphinx theme" -name = "alabaster" -optional = false -python-versions = "*" -version = "0.7.12" - -[[package]] -category = "main" -description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)." -name = "apispec" -optional = false -python-versions = ">=3.5" -version = "3.3.2" - -[package.extras] -dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["marshmallow (>=2.19.2)", "pyyaml (5.3.1)", "sphinx (3.2.1)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.5.0)"] -lint = ["flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] -tests = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"] -validation = ["prance (>=0.11)"] -yaml = ["PyYAML (>=3.10)"] - -[[package]] -category = "main" -description = "Web framework plugins for apispec." -name = "apispec-webframeworks" -optional = false -python-versions = ">=3.6" -version = "0.5.2" - -[package.dependencies] -[package.dependencies.apispec] -extras = ["yaml"] -version = ">=2.0.0" - -[package.extras] -dev = ["pytest", "mock", "Flask (1.1.1)", "tornado", "bottle (0.12.17)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.18,<2.0)", "tox"] -lint = ["flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.18,<2.0)"] -tests = ["pytest", "mock", "Flask (1.1.1)", "tornado", "bottle (0.12.17)"] - -[[package]] -category = "dev" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -name = "appdirs" -optional = false -python-versions = "*" -version = "1.4.4" - -[[package]] -category = "dev" -description = "An abstract syntax tree for Python with inference support." -name = "astroid" -optional = false -python-versions = ">=3.5" -version = "2.4.2" - -[package.dependencies] -lazy-object-proxy = ">=1.4.0,<1.5.0" -six = ">=1.12,<2.0" -wrapt = ">=1.11,<2.0" - -[package.dependencies.typed-ast] -python = "<3.8" -version = ">=1.4.0,<1.5" - -[[package]] -category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" -name = "atomicwrites" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" - -[[package]] -category = "dev" -description = "Classes Without Boilerplate" -name = "attrs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.1.0" - -[package.extras] -dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] - -[[package]] -category = "dev" -description = "Internationalization utilities" -name = "babel" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8.0" - -[package.dependencies] -pytz = ">=2015.7" - -[[package]] -category = "dev" -description = "The uncompromising code formatter." -name = "black" -optional = false -python-versions = ">=3.6" -version = "20.8b1" - -[package.dependencies] -appdirs = "*" -click = ">=7.1.2" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.6,<1" -regex = ">=2020.1.8" -toml = ">=0.10.1" -typed-ast = ">=1.4.0" -typing-extensions = ">=3.7.4" - -[package.dependencies.dataclasses] -python = "<3.7" -version = ">=0.6" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] - -[[package]] -category = "dev" -description = "Python package for providing Mozilla's CA Bundle." -name = "certifi" -optional = false -python-versions = "*" -version = "2020.6.20" - -[[package]] -category = "dev" -description = "Universal encoding detector for Python 2 and 3" -name = "chardet" -optional = false -python-versions = "*" -version = "3.0.4" - -[[package]] -category = "main" -description = "Composable command line interface toolkit" -name = "click" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" - -[[package]] -category = "dev" -description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\"" -name = "colorama" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" - -[[package]] -category = "dev" -description = "Code coverage measurement for Python" -name = "coverage" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2.1" - -[package.extras] -toml = ["toml"] - -[[package]] -category = "dev" -description = "A backport of the dataclasses module for Python 3.6" -marker = "python_version < \"3.7\"" -name = "dataclasses" -optional = false -python-versions = "*" -version = "0.6" - -[[package]] -category = "dev" -description = "Docutils -- Python Documentation Utilities" -name = "docutils" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.16" - -[[package]] -category = "main" -description = "A simple framework for building complex web applications." -name = "flask" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.1.2" - -[package.dependencies] -Jinja2 = ">=2.10.1" -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" -name = "flask-cors" -optional = false -python-versions = "*" -version = "3.0.9" - -[package.dependencies] -Flask = ">=0.9" -Six = "*" - -[[package]] -category = "dev" -description = "Internationalized Domain Names in Applications (IDNA)" -name = "idna" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10" - -[[package]] -category = "main" -description = "Cross-platform network interface and IP address enumeration library" -name = "ifaddr" -optional = false -python-versions = "*" -version = "0.1.7" - -[[package]] -category = "dev" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -name = "imagesize" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.2.0" - -[[package]] -category = "dev" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" -name = "importlib-metadata" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] - -[[package]] -category = "dev" -description = "iniconfig: brain-dead simple config-ini parsing" -name = "iniconfig" -optional = false -python-versions = "*" -version = "1.0.1" - -[[package]] -category = "dev" -description = "A Python utility / library to sort Python imports." -name = "isort" -optional = false -python-versions = ">=3.6,<4.0" -version = "5.5.1" - -[package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] - -[[package]] -category = "main" -description = "Various helpers to pass data to untrusted environments and back." -name = "itsdangerous" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.0" - -[[package]] -category = "main" -description = "A very fast and expressive template engine." -name = "jinja2" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.11.2" - -[package.dependencies] -MarkupSafe = ">=0.23" - -[package.extras] -i18n = ["Babel (>=0.8)"] - -[[package]] -category = "dev" -description = "An implementation of JSON Schema validation for Python" -name = "jsonschema" -optional = false -python-versions = "*" -version = "3.2.0" - -[package.dependencies] -attrs = ">=17.4.0" -pyrsistent = ">=0.14.0" -setuptools = "*" -six = ">=1.11.0" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - -[package.extras] -format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] - -[[package]] -category = "dev" -description = "A fast and thorough lazy object proxy." -name = "lazy-object-proxy" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.3" - -[[package]] -category = "main" -description = "Safely add untrusted strings to HTML/XML markup." -name = "markupsafe" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.1.1" - -[[package]] -category = "main" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -name = "marshmallow" -optional = false -python-versions = ">=3.5" -version = "3.7.1" - -[package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (3.1.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.1.13)"] -lint = ["mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] -tests = ["pytest", "pytz", "simplejson"] - -[[package]] -category = "dev" -description = "McCabe checker, plugin for flake8" -name = "mccabe" -optional = false -python-versions = "*" -version = "0.6.1" - -[[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" -optional = false -python-versions = ">=3.5" -version = "8.5.0" - -[[package]] -category = "dev" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -name = "mypy-extensions" -optional = false -python-versions = "*" -version = "0.4.3" - -[[package]] -category = "dev" -description = "NumPy is the fundamental package for array computing with Python." -name = "numpy" -optional = false -python-versions = ">=3.6" -version = "1.19.1" - -[[package]] -category = "dev" -description = "Core utilities for Python packages" -name = "packaging" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.4" - -[package.dependencies] -pyparsing = ">=2.0.2" -six = "*" - -[[package]] -category = "dev" -description = "Utility library for gitignore style pattern matching of file paths." -name = "pathspec" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" - -[[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" -name = "pluggy" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.1" - -[package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -name = "py" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.9.0" - -[[package]] -category = "dev" -description = "Pygments is a syntax highlighting package written in Python." -name = "pygments" -optional = false -python-versions = ">=3.5" -version = "2.6.1" - -[[package]] -category = "dev" -description = "python code static checker" -name = "pylint" -optional = false -python-versions = ">=3.5.*" -version = "2.6.0" - -[package.dependencies] -astroid = ">=2.4.0,<=2.5" -colorama = "*" -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.7" -toml = ">=0.7.1" - -[[package]] -category = "dev" -description = "Python parsing module" -name = "pyparsing" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" - -[[package]] -category = "dev" -description = "Persistent/Functional/Immutable data structures" -name = "pyrsistent" -optional = false -python-versions = "*" -version = "0.16.0" - -[package.dependencies] -six = "*" - -[[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" -name = "pytest" -optional = false -python-versions = ">=3.5" -version = "6.0.1" - -[package.dependencies] -atomicwrites = ">=1.0" -attrs = ">=17.4.0" -colorama = "*" -iniconfig = "*" -more-itertools = ">=4.0.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.8.2" -toml = "*" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - -[package.extras] -checkqa_mypy = ["mypy (0.780)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] - -[[package]] -category = "dev" -description = "Pytest plugin for measuring coverage." -name = "pytest-cov" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.1" - -[package.dependencies] -coverage = ">=4.4" -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] - -[[package]] -category = "dev" -description = "World timezone definitions, modern and historical" -name = "pytz" -optional = false -python-versions = "*" -version = "2020.1" - -[[package]] -category = "dev" -description = "YAML parser and emitter for Python" -name = "pyyaml" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.3.1" - -[[package]] -category = "dev" -description = "Alternative regular expression module, to replace re." -name = "regex" -optional = false -python-versions = "*" -version = "2020.7.14" - -[[package]] -category = "dev" -description = "Python HTTP for Humans." -name = "requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.24.0" - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" -idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" - -[package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] - -[[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" -name = "six" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" - -[[package]] -category = "dev" -description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." -name = "snowballstemmer" -optional = false -python-versions = "*" -version = "2.0.0" - -[[package]] -category = "dev" -description = "Python documentation generator" -name = "sphinx" -optional = false -python-versions = ">=3.5" -version = "3.2.1" - -[package.dependencies] -Jinja2 = ">=2.3" -Pygments = ">=2.0" -alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = ">=0.3.5" -docutils = ">=0.12" -imagesize = "*" -packaging = "*" -requests = ">=2.5.0" -setuptools = "*" -snowballstemmer = ">=1.1" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = "*" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = "*" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] -test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] - -[[package]] -category = "dev" -description = "Sphinx API documentation generator" -name = "sphinx-autoapi" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -version = "1.5.0" - -[package.dependencies] -Jinja2 = "*" -PyYAML = "*" -sphinx = ">=1.6" -unidecode = "*" - -[package.dependencies.astroid] -python = ">=3" -version = "*" - -[package.extras] -dotnet = ["sphinxcontrib-dotnetdomain"] -go = ["sphinxcontrib-golangdomain"] - -[[package]] -category = "dev" -description = "Read the Docs theme for Sphinx" -name = "sphinx-rtd-theme" -optional = false -python-versions = "*" -version = "0.5.0" - -[package.dependencies] -sphinx = "*" - -[package.extras] -dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] - -[[package]] -category = "dev" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -name = "sphinxcontrib-applehelp" -optional = false -python-versions = ">=3.5" -version = "1.0.2" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -category = "dev" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -name = "sphinxcontrib-devhelp" -optional = false -python-versions = ">=3.5" -version = "1.0.2" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -category = "dev" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -name = "sphinxcontrib-htmlhelp" -optional = false -python-versions = ">=3.5" -version = "1.0.3" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] - -[[package]] -category = "dev" -description = "A sphinx extension which renders display math in HTML via JavaScript" -name = "sphinxcontrib-jsmath" -optional = false -python-versions = ">=3.5" -version = "1.0.1" - -[package.extras] -test = ["pytest", "flake8", "mypy"] - -[[package]] -category = "dev" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -name = "sphinxcontrib-qthelp" -optional = false -python-versions = ">=3.5" -version = "1.0.3" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -category = "dev" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -name = "sphinxcontrib-serializinghtml" -optional = false -python-versions = ">=3.5" -version = "1.1.4" - -[package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest"] - -[[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" -optional = false -python-versions = "*" -version = "0.10.1" - -[[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" -name = "typed-ast" -optional = false -python-versions = "*" -version = "1.4.1" - -[[package]] -category = "dev" -description = "Backported and Experimental Type Hints for Python 3.5+" -name = "typing-extensions" -optional = false -python-versions = "*" -version = "3.7.4.3" - -[[package]] -category = "dev" -description = "ASCII transliterations of Unicode text" -name = "unidecode" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.1" - -[[package]] -category = "dev" -description = "HTTP library with thread-safe connection pooling, file post, and more." -name = "urllib3" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.10" - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "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." -name = "webargs" -optional = false -python-versions = ">=3.5" -version = "6.1.0" - -[package.dependencies] -marshmallow = ">=2.15.2" - -[package.extras] -dev = ["pytest", "webtest (2.0.35)", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "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 (>=2.0.0)", "aiohttp (>=3.0.0)", "mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox", "mock"] -docs = ["Sphinx (3.0.3)", "sphinx-issues (1.2.0)", "sphinx-typlog-theme (0.8.0)", "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 (>=2.0.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 (>=2.0.0)", "aiohttp (>=3.0.0)"] -lint = ["mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"] -tests = ["pytest", "webtest (2.0.35)", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "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 (>=2.0.0)", "aiohttp (>=3.0.0)", "mock"] - -[[package]] -category = "main" -description = "The comprehensive WSGI web application library." -name = "werkzeug" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.0.1" - -[package.extras] -dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] -watchdog = ["watchdog"] - -[[package]] -category = "dev" -description = "Module for decorators, wrappers and monkey patching." -name = "wrapt" -optional = false -python-versions = "*" -version = "1.12.1" - -[[package]] -category = "main" -description = "Pure Python Multicast DNS Service Discovery Library (Bonjour/Avahi compatible)" -name = "zeroconf" -optional = false -python-versions = "*" -version = "0.28.3" - -[package.dependencies] -ifaddr = ">=0.1.7" - -[[package]] -category = "dev" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" -name = "zipp" -optional = false -python-versions = ">=3.6" -version = "3.1.0" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] - -[metadata] -content-hash = "8ec717b2c415c44985dceece39bb1fc71aef44b255f5f6c39691be31973d6ab8" -python-versions = "^3.6" - -[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.3.2-py2.py3-none-any.whl", hash = "sha256:a1df9ec6b2cd0edf45039ef025abd7f0660808fa2edf737d3ba1cf5ef1a4625b"}, - {file = "apispec-3.3.2.tar.gz", hash = "sha256:d23ebd5b71e541e031b02a19db10b5e6d5ef8452c552833e3e1afc836b40b1ad"}, -] -apispec-webframeworks = [ - {file = "apispec-webframeworks-0.5.2.tar.gz", hash = "sha256:0db35b267914b3f8c562aca0261957dbcb4176f255eacc22520277010818dcf3"}, - {file = "apispec_webframeworks-0.5.2-py2.py3-none-any.whl", hash = "sha256:482c563abbcc2a261439476cb3f1a7c7284cc997c322c574d48c111643e9c04e"}, -] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -astroid = [ - {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, - {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, - {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, -] -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-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, - {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, -] -certifi = [ - {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, - {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, -] -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.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, -] -colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -coverage = [ - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, - {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, - {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, - {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, - {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, - {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, - {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, - {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, - {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, - {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, - {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, - {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, - {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, - {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, - {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, - {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, - {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, - {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, - {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, -] -dataclasses = [ - {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, - {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, -] -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.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, - {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, -] -flask-cors = [ - {file = "Flask-Cors-3.0.9.tar.gz", hash = "sha256:6bcfc100288c5d1bcb1dbb854babd59beee622ffd321e444b05f24d6d58466b8"}, - {file = "Flask_Cors-3.0.9-py2.py3-none-any.whl", hash = "sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324"}, -] -idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, -] -ifaddr = [ - {file = "ifaddr-0.1.7-py2.py3-none-any.whl", hash = "sha256:d1f603952f0a71c9ab4e705754511e4e03b02565bc4cec7188ad6415ff534cd3"}, - {file = "ifaddr-0.1.7.tar.gz", hash = "sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94"}, -] -imagesize = [ - {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, - {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, -] -importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, -] -iniconfig = [ - {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, - {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, -] -isort = [ - {file = "isort-5.5.1-py3-none-any.whl", hash = "sha256:a200d47b7ee8b7f7d0a9646650160c4a51b6a91a9413fd31b1da2c4de789f5d3"}, - {file = "isort-5.5.1.tar.gz", hash = "sha256:92533892058de0306e51c88f22ece002a209dc8e80288aa3cec6d443060d584f"}, -] -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.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, - {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, -] -jsonschema = [ - {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, - {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, -] -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-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, -] -marshmallow = [ - {file = "marshmallow-3.7.1-py2.py3-none-any.whl", hash = "sha256:67bf4cae9d3275b3fc74bd7ff88a7c98ee8c57c94b251a67b031dc293ecc4b76"}, - {file = "marshmallow-3.7.1.tar.gz", hash = "sha256:a2a5eefb4b75a3b43f05be1cca0b6686adf56af7465c3ca629e5ad8d1e1fe13d"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -more-itertools = [ - {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, - {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -numpy = [ - {file = "numpy-1.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff"}, - {file = "numpy-1.19.1-cp36-cp36m-win32.whl", hash = "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624"}, - {file = "numpy-1.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983"}, - {file = "numpy-1.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954"}, - {file = "numpy-1.19.1-cp37-cp37m-win32.whl", hash = "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b"}, - {file = "numpy-1.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055"}, - {file = "numpy-1.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd"}, - {file = "numpy-1.19.1-cp38-cp38-win32.whl", hash = "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae"}, - {file = "numpy-1.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc"}, - {file = "numpy-1.19.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1"}, - {file = "numpy-1.19.1.zip", hash = "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491"}, -] -packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, -] -pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, -] -pygments = [ - {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, - {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, -] -pylint = [ - {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, - {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, -] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] -pyrsistent = [ - {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"}, -] -pytest = [ - {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, - {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, -] -pytest-cov = [ - {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, - {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, -] -pytz = [ - {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, - {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, -] -pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, -] -regex = [ - {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, - {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, - {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, - {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, - {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, - {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, - {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, - {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, - {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, -] -requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, -] -six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -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-3.2.1-py3-none-any.whl", hash = "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0"}, - {file = "Sphinx-3.2.1.tar.gz", hash = "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8"}, -] -sphinx-autoapi = [ - {file = "sphinx-autoapi-1.5.0.tar.gz", hash = "sha256:95ffb69e7618284d84d4947a1cd386c1829e43e0648e80d6fc669737813609c3"}, - {file = "sphinx_autoapi-1.5.0-py2.py3-none-any.whl", hash = "sha256:8a32c6cc26ed0bc0c15c37bce1da9f745f0765741a302e918e9c91775ae69b04"}, -] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-0.5.0-py2.py3-none-any.whl", hash = "sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"}, - {file = "sphinx_rtd_theme-0.5.0.tar.gz", hash = "sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, - {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, -] -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.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, - {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, -] -toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, -] -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"}, -] -typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, -] -unidecode = [ - {file = "Unidecode-1.1.1-py2.py3-none-any.whl", hash = "sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a"}, - {file = "Unidecode-1.1.1.tar.gz", hash = "sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8"}, -] -urllib3 = [ - {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, - {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, -] -webargs = [ - {file = "webargs-6.1.0-py2.py3-none-any.whl", hash = "sha256:cf0b5e2fdfb81f28b9332fce15621069d3fbc910a01c7ca8a5e166371699927b"}, - {file = "webargs-6.1.0.tar.gz", hash = "sha256:ebb47fb35c3c4fc764213a17d1686e82fec54759ebed2b0715907d566668bb3f"}, -] -werkzeug = [ - {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, - {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, -] -wrapt = [ - {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, -] -zeroconf = [ - {file = "zeroconf-0.28.3-py3-none-any.whl", hash = "sha256:9ae1d9fbb53e1e5b0965a1e60873f99568f0424a03eb28ce793e75c999a8fc50"}, - {file = "zeroconf-0.28.3.tar.gz", hash = "sha256:1a160082ef198884331b04b636132a243a06d3155f6664e96704b6984cbf485c"}, -] -zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, -] +[[package]] +category = "dev" +description = "A configurable sidebar-enabled Sphinx theme" +name = "alabaster" +optional = false +python-versions = "*" +version = "0.7.12" + +[[package]] +category = "main" +description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)." +name = "apispec" +optional = false +python-versions = ">=3.5" +version = "3.3.2" + +[package.extras] +dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["marshmallow (>=2.19.2)", "pyyaml (5.3.1)", "sphinx (3.2.1)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.5.0)"] +lint = ["flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] +tests = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"] +validation = ["prance (>=0.11)"] +yaml = ["PyYAML (>=3.10)"] + +[[package]] +category = "main" +description = "Web framework plugins for apispec." +name = "apispec-webframeworks" +optional = false +python-versions = ">=3.6" +version = "0.5.2" + +[package.dependencies] +[package.dependencies.apispec] +extras = ["yaml"] +version = ">=2.0.0" + +[package.extras] +dev = ["pytest", "mock", "Flask (1.1.1)", "tornado", "bottle (0.12.17)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.18,<2.0)", "tox"] +lint = ["flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.18,<2.0)"] +tests = ["pytest", "mock", "Flask (1.1.1)", "tornado", "bottle (0.12.17)"] + +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "dev" +description = "An abstract syntax tree for Python with inference support." +name = "astroid" +optional = false +python-versions = ">=3.5" +version = "2.4.2" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0,<1.5.0" +six = ">=1.12,<2.0" +wrapt = ">=1.11,<2.0" + +[package.dependencies.typed-ast] +python = "<3.8" +version = ">=1.4.0,<1.5" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.1.0" + +[package.extras] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "Internationalization utilities" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.0" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "20.8b1" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.dependencies.dataclasses] +python = "<3.7" +version = ">=0.6" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "dev" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.6.20" + +[[package]] +category = "dev" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.2.1" + +[package.extras] +toml = ["toml"] + +[[package]] +category = "dev" +description = "A backport of the dataclasses module for Python 3.6" +marker = "python_version < \"3.7\"" +name = "dataclasses" +optional = false +python-versions = "*" +version = "0.6" + +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.16" + +[[package]] +category = "main" +description = "A simple framework for building complex web applications." +name = "flask" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.1.2" + +[package.dependencies] +Jinja2 = ">=2.10.1" +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" +name = "flask-cors" +optional = false +python-versions = "*" +version = "3.0.9" + +[package.dependencies] +Flask = ">=0.9" +Six = "*" + +[[package]] +category = "dev" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.10" + +[[package]] +category = "main" +description = "Cross-platform network interface and IP address enumeration library" +name = "ifaddr" +optional = false +python-versions = "*" +version = "0.1.7" + +[[package]] +category = "dev" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +name = "imagesize" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.2.0" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.7.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] + +[[package]] +category = "dev" +description = "iniconfig: brain-dead simple config-ini parsing" +name = "iniconfig" +optional = false +python-versions = "*" +version = "1.0.1" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=3.6,<4.0" +version = "5.5.1" + +[package.extras] +colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] + +[[package]] +category = "main" +description = "Various helpers to pass data to untrusted environments and back." +name = "itsdangerous" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "main" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "dev" +description = "An implementation of JSON Schema validation for Python" +name = "jsonschema" +optional = false +python-versions = "*" +version = "3.2.0" + +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0" +setuptools = "*" +six = ">=1.11.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + +[package.extras] +format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] +format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] + +[[package]] +category = "dev" +description = "A fast and thorough lazy object proxy." +name = "lazy-object-proxy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.3" + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "main" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +name = "marshmallow" +optional = false +python-versions = ">=3.5" +version = "3.7.1" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (3.1.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.1.13)"] +lint = ["mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.5.0" + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "NumPy is the fundamental package for array computing with Python." +name = "numpy" +optional = false +python-versions = ">=3.6" +version = "1.19.1" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.4" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.9.0" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=3.5" +version = "2.6.1" + +[[package]] +category = "dev" +description = "python code static checker" +name = "pylint" +optional = false +python-versions = ">=3.5.*" +version = "2.6.0" + +[package.dependencies] +astroid = ">=2.4.0,<=2.5" +colorama = "*" +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.7" + +[[package]] +category = "dev" +description = "Persistent/Functional/Immutable data structures" +name = "pyrsistent" +optional = false +python-versions = "*" +version = "0.16.0" + +[package.dependencies] +six = "*" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "6.0.1" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +iniconfig = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.8.2" +toml = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +checkqa_mypy = ["mypy (0.780)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.10.1" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] + +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2020.1" + +[[package]] +category = "dev" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.7.14" + +[[package]] +category = "dev" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.24.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Python documentation generator" +name = "sphinx" +optional = false +python-versions = ">=3.5" +version = "3.2.1" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = ">=0.3.5" +docutils = ">=0.12" +imagesize = "*" +packaging = "*" +requests = ">=2.5.0" +setuptools = "*" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] + +[[package]] +category = "dev" +description = "Sphinx API documentation generator" +name = "sphinx-autoapi" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +version = "1.5.0" + +[package.dependencies] +Jinja2 = "*" +PyYAML = "*" +sphinx = ">=1.6" +unidecode = "*" + +[package.dependencies.astroid] +python = ">=3" +version = "*" + +[package.extras] +dotnet = ["sphinxcontrib-dotnetdomain"] +go = ["sphinxcontrib-golangdomain"] + +[[package]] +category = "dev" +description = "Read the Docs theme for Sphinx" +name = "sphinx-rtd-theme" +optional = false +python-versions = "*" +version = "0.5.0" + +[package.dependencies] +sphinx = "*" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + +[[package]] +category = "dev" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +name = "sphinxcontrib-applehelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +name = "sphinxcontrib-devhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +name = "sphinxcontrib-htmlhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +category = "dev" +description = "A sphinx extension which renders display math in HTML via JavaScript" +name = "sphinxcontrib-jsmath" +optional = false +python-versions = ">=3.5" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +name = "sphinxcontrib-qthelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +name = "sphinxcontrib-serializinghtml" +optional = false +python-versions = ">=3.5" +version = "1.1.4" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4.3" + +[[package]] +category = "dev" +description = "ASCII transliterations of Unicode text" +name = "unidecode" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.10" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "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." +name = "webargs" +optional = false +python-versions = ">=3.5" +version = "6.1.0" + +[package.dependencies] +marshmallow = ">=2.15.2" + +[package.extras] +dev = ["pytest", "webtest (2.0.35)", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "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 (>=2.0.0)", "aiohttp (>=3.0.0)", "mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox", "mock"] +docs = ["Sphinx (3.0.3)", "sphinx-issues (1.2.0)", "sphinx-typlog-theme (0.8.0)", "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 (>=2.0.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 (>=2.0.0)", "aiohttp (>=3.0.0)"] +lint = ["mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"] +tests = ["pytest", "webtest (2.0.35)", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "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 (>=2.0.0)", "aiohttp (>=3.0.0)", "mock"] + +[[package]] +category = "main" +description = "The comprehensive WSGI web application library." +name = "werkzeug" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.0.1" + +[package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +watchdog = ["watchdog"] + +[[package]] +category = "dev" +description = "Module for decorators, wrappers and monkey patching." +name = "wrapt" +optional = false +python-versions = "*" +version = "1.12.1" + +[[package]] +category = "main" +description = "Pure Python Multicast DNS Service Discovery Library (Bonjour/Avahi compatible)" +name = "zeroconf" +optional = false +python-versions = "*" +version = "0.28.3" + +[package.dependencies] +ifaddr = ">=0.1.7" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=3.6" +version = "3.1.0" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["jaraco.itertools", "func-timeout"] + +[metadata] +content-hash = "8ec717b2c415c44985dceece39bb1fc71aef44b255f5f6c39691be31973d6ab8" +python-versions = "^3.6" + +[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.3.2-py2.py3-none-any.whl", hash = "sha256:a1df9ec6b2cd0edf45039ef025abd7f0660808fa2edf737d3ba1cf5ef1a4625b"}, + {file = "apispec-3.3.2.tar.gz", hash = "sha256:d23ebd5b71e541e031b02a19db10b5e6d5ef8452c552833e3e1afc836b40b1ad"}, +] +apispec-webframeworks = [ + {file = "apispec-webframeworks-0.5.2.tar.gz", hash = "sha256:0db35b267914b3f8c562aca0261957dbcb4176f255eacc22520277010818dcf3"}, + {file = "apispec_webframeworks-0.5.2-py2.py3-none-any.whl", hash = "sha256:482c563abbcc2a261439476cb3f1a7c7284cc997c322c574d48c111643e9c04e"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +astroid = [ + {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, + {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, + {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, +] +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-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +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.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +coverage = [ + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, + {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, + {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, + {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, + {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, + {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, + {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, + {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, + {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, + {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, + {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, + {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, + {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, + {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, + {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, + {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, + {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, + {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, + {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, +] +dataclasses = [ + {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, + {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, +] +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.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, + {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, +] +flask-cors = [ + {file = "Flask-Cors-3.0.9.tar.gz", hash = "sha256:6bcfc100288c5d1bcb1dbb854babd59beee622ffd321e444b05f24d6d58466b8"}, + {file = "Flask_Cors-3.0.9-py2.py3-none-any.whl", hash = "sha256:cee4480aaee421ed029eaa788f4049e3e26d15b5affb6a880dade6bafad38324"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +ifaddr = [ + {file = "ifaddr-0.1.7-py2.py3-none-any.whl", hash = "sha256:d1f603952f0a71c9ab4e705754511e4e03b02565bc4cec7188ad6415ff534cd3"}, + {file = "ifaddr-0.1.7.tar.gz", hash = "sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, + {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, +] +iniconfig = [ + {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, + {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, +] +isort = [ + {file = "isort-5.5.1-py3-none-any.whl", hash = "sha256:a200d47b7ee8b7f7d0a9646650160c4a51b6a91a9413fd31b1da2c4de789f5d3"}, + {file = "isort-5.5.1.tar.gz", hash = "sha256:92533892058de0306e51c88f22ece002a209dc8e80288aa3cec6d443060d584f"}, +] +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.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +jsonschema = [ + {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, + {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, +] +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-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +marshmallow = [ + {file = "marshmallow-3.7.1-py2.py3-none-any.whl", hash = "sha256:67bf4cae9d3275b3fc74bd7ff88a7c98ee8c57c94b251a67b031dc293ecc4b76"}, + {file = "marshmallow-3.7.1.tar.gz", hash = "sha256:a2a5eefb4b75a3b43f05be1cca0b6686adf56af7465c3ca629e5ad8d1e1fe13d"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"}, + {file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +numpy = [ + {file = "numpy-1.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff"}, + {file = "numpy-1.19.1-cp36-cp36m-win32.whl", hash = "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624"}, + {file = "numpy-1.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983"}, + {file = "numpy-1.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954"}, + {file = "numpy-1.19.1-cp37-cp37m-win32.whl", hash = "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b"}, + {file = "numpy-1.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055"}, + {file = "numpy-1.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd"}, + {file = "numpy-1.19.1-cp38-cp38-win32.whl", hash = "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae"}, + {file = "numpy-1.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc"}, + {file = "numpy-1.19.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1"}, + {file = "numpy-1.19.1.zip", hash = "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, +] +pygments = [ + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, +] +pylint = [ + {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, + {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyrsistent = [ + {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"}, +] +pytest = [ + {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, + {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, +] +pytest-cov = [ + {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, + {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +regex = [ + {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, + {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, + {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, + {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, + {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, + {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, + {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, + {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +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-3.2.1-py3-none-any.whl", hash = "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0"}, + {file = "Sphinx-3.2.1.tar.gz", hash = "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8"}, +] +sphinx-autoapi = [ + {file = "sphinx-autoapi-1.5.0.tar.gz", hash = "sha256:95ffb69e7618284d84d4947a1cd386c1829e43e0648e80d6fc669737813609c3"}, + {file = "sphinx_autoapi-1.5.0-py2.py3-none-any.whl", hash = "sha256:8a32c6cc26ed0bc0c15c37bce1da9f745f0765741a302e918e9c91775ae69b04"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.5.0-py2.py3-none-any.whl", hash = "sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"}, + {file = "sphinx_rtd_theme-0.5.0.tar.gz", hash = "sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +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.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +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"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +unidecode = [ + {file = "Unidecode-1.1.1-py2.py3-none-any.whl", hash = "sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a"}, + {file = "Unidecode-1.1.1.tar.gz", hash = "sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8"}, +] +urllib3 = [ + {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, + {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, +] +webargs = [ + {file = "webargs-6.1.0-py2.py3-none-any.whl", hash = "sha256:cf0b5e2fdfb81f28b9332fce15621069d3fbc910a01c7ca8a5e166371699927b"}, + {file = "webargs-6.1.0.tar.gz", hash = "sha256:ebb47fb35c3c4fc764213a17d1686e82fec54759ebed2b0715907d566668bb3f"}, +] +werkzeug = [ + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, +] +wrapt = [ + {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, +] +zeroconf = [ + {file = "zeroconf-0.28.3-py3-none-any.whl", hash = "sha256:9ae1d9fbb53e1e5b0965a1e60873f99568f0424a03eb28ce793e75c999a8fc50"}, + {file = "zeroconf-0.28.3.tar.gz", hash = "sha256:1a160082ef198884331b04b636132a243a06d3155f6664e96704b6984cbf485c"}, +] +zipp = [ + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, +] diff --git a/pyproject.toml b/pyproject.toml index ddd8371a..b26cfd4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,50 +1,49 @@ -[tool.poetry] -name = "labthings" -version = "0.8.0" -description = "Python implementation of LabThings, based on the Flask microframework" -readme = "README.md" -repository = "https://github.com/labthings/python-labthings/" -authors = ["Joel Collins "] -classifiers = [ - "Topic :: System :: Hardware", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Topic :: Internet :: WWW/HTTP :: WSGI" -] - -[tool.poetry.dependencies] -python = "^3.6" -Flask = "^1.1.1" -marshmallow = "^3.4.0" -webargs = "^6.0.0" -apispec = "^3.2.0" -flask-cors = "^3.0.8" -zeroconf = ">=0.24.5,<0.29.0" -flask-threaded-sockets = "^0.3.0" -apispec_webframeworks = "^0.5.2" - -[tool.poetry.dev-dependencies] -pytest = "^6.0" -black = {version = "^20.8b1",allow-prereleases = true} -pytest-cov = "^2.10.1" -numpy = "^1.19.1" -jsonschema = "^3.2.0" -pylint = "^2.6.0" -sphinx = "^3.2.1" -sphinx-autoapi = "^1.4.0" -sphinx-rtd-theme = "^0.5.0" - -[tool.black] -exclude = '(\.eggs|\.git|\.venv)' - -[tool.isort] -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true -line_length = 88 - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" - +[tool.poetry] +name = "labthings" +version = "0.8.0" +description = "Python implementation of LabThings, based on the Flask microframework" +readme = "README.md" +repository = "https://github.com/labthings/python-labthings/" +authors = ["Joel Collins "] +classifiers = [ + "Topic :: System :: Hardware", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Internet :: WWW/HTTP :: WSGI" +] + +[tool.poetry.dependencies] +python = "^3.6" +Flask = "^1.1.1" +marshmallow = "^3.4.0" +webargs = "^6.0.0" +apispec = "^3.2.0" +flask-cors = "^3.0.8" +zeroconf = ">=0.24.5,<0.29.0" +apispec_webframeworks = "^0.5.2" + +[tool.poetry.dev-dependencies] +pytest = "^6.0" +black = {version = "^20.8b1",allow-prereleases = true} +pytest-cov = "^2.10.1" +numpy = "^1.19.1" +jsonschema = "^3.2.0" +pylint = "^2.6.0" +sphinx = "^3.2.1" +sphinx-autoapi = "^1.4.0" +sphinx-rtd-theme = "^0.5.0" + +[tool.black] +exclude = '(\.eggs|\.git|\.venv)' + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 88 + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + diff --git a/src/labthings/__init__.py b/src/labthings/__init__.py index faf18d08..60f0b05b 100644 --- a/src/labthings/__init__.py +++ b/src/labthings/__init__.py @@ -1,62 +1,63 @@ -# Main LabThing class -# Submodules -from . import extensions, fields, json, marshalling, views - -# Action threads -from .actions import ( - ActionKilledException, - current_action, - update_action_data, - update_action_progress, -) - -# Functions to speed up finding global objects -from .find import ( - current_labthing, - find_component, - find_extension, - registered_components, - registered_extensions, -) -from .labthing import LabThing - -# Quick-create app+LabThing function -from .quick import create_app - -# Schema and field -from .schema import Schema - -# Synchronisation classes -from .sync import ClientEvent, CompositeLock, StrictLock - -# Views -from .views import ActionView, PropertyView - -# Suggested WSGI+WebSocket server class -from .wsgi import Server - -__all__ = [ - "LabThing", - "create_app", - "Server", - "current_labthing", - "registered_extensions", - "registered_components", - "find_extension", - "find_component", - "StrictLock", - "CompositeLock", - "ClientEvent", - "current_action", - "update_action_progress", - "update_action_data", - "ActionKilledException", - "extensions", - "views", - "fields", - "Schema", - "semantics", - "json", - "PropertyView", - "ActionView", -] +# Main LabThing class +# Submodules +from . import extensions, fields, json, marshalling, views + +# Action threads +from .actions import ( + ActionKilledException, + current_action, + update_action_data, + update_action_progress, +) + +# Functions to speed up finding global objects +from .find import ( + current_labthing, + find_component, + find_extension, + registered_components, + registered_extensions, +) +from .labthing import LabThing + +# Quick-create app+LabThing function +from .quick import create_app + +# Schema and field +from .schema import Schema + +# Synchronisation classes +from .sync import ClientEvent, CompositeLock, StrictLock + +# Views +from .views import ActionView, PropertyView, op + +# Suggested WSGI server class +from .wsgi import Server + +__all__ = [ + "LabThing", + "create_app", + "Server", + "current_labthing", + "registered_extensions", + "registered_components", + "find_extension", + "find_component", + "StrictLock", + "CompositeLock", + "ClientEvent", + "current_action", + "update_action_progress", + "update_action_data", + "ActionKilledException", + "extensions", + "views", + "fields", + "Schema", + "semantics", + "json", + "PropertyView", + "ActionView", + "op" +] diff --git a/src/labthings/default_views/sockets.py b/src/labthings/default_views/sockets.py deleted file mode 100644 index efb344dc..00000000 --- a/src/labthings/default_views/sockets.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging - -from ..find import current_labthing -from ..sockets import SocketSubscriber - -STATIC_SOCKET_RESPONSES = {"__unittest": "__unittest_response"} - - -def socket_handler(ws): - """ - - :param ws: - - """ - # Create a socket subscriber - wssub = SocketSubscriber(ws) - current_labthing().subscribers.add(wssub) - logging.info(f"Added subscriber {wssub}") - # Start the socket connection handler loop - while not ws.closed: - message = ws.receive() - if message is None: - break - response = process_socket_message(message) - if response: - ws.send(response) - # Remove the subscriber once the loop returns - current_labthing().subscribers.remove(wssub) - logging.info(f"Removed subscriber {wssub}") - - -def process_socket_message(message: str): - """ - - :param message: str: - - """ - if message: - if message in STATIC_SOCKET_RESPONSES: - return STATIC_SOCKET_RESPONSES.get(message) - else: - return None diff --git a/src/labthings/labthing.py b/src/labthings/labthing.py index 8231bc02..01ac8997 100644 --- a/src/labthings/labthing.py +++ b/src/labthings/labthing.py @@ -1,533 +1,510 @@ -import logging -import uuid -import weakref - -from apispec import APISpec -from apispec_webframeworks.flask import FlaskPlugin -from flask import url_for -from flask_threaded_sockets import Sockets - -from .actions.pool import Pool -from .apispec import FlaskLabThingsPlugin, MarshmallowPlugin -from .default_views.actions import ActionObjectView, ActionQueueView -from .default_views.docs import SwaggerUIView, docs_blueprint -from .default_views.extensions import ExtensionList -from .default_views.root import RootView -from .default_views.sockets import socket_handler -from .event import Event -from .extensions import BaseExtension -from .httperrorhandler import SerializedExceptionHandler -from .json.encoder import LabThingsJSONEncoder -from .logging import LabThingLogger -from .names import ( - ACTION_ENDPOINT, - ACTION_LIST_ENDPOINT, - EXTENSION_LIST_ENDPOINT, - EXTENSION_NAME, -) -from .representations import DEFAULT_REPRESENTATIONS -from .td import ThingDescription -from .utilities import camel_to_snake, clean_url_string -from .views import ActionView, PropertyView - -# from apispec.ext.marshmallow import MarshmallowPlugin - - -class LabThing: - """ - The main entry point for the application. - You need to initialize it with a Flask Application: :: - - >>> app = Flask(__name__) - >>> labthing = labthings.LabThing(app) - - Alternatively, you can use :meth:`init_app` to set the Flask application - after it has been constructed. - - :param app: the Flask application object - :type app: flask.Flask - :param prefix: Prefix all routes with a value, eg v1 or 2010-04-01 - :type prefix: str - :param title: Human-readable title of the Thing - :type title: str - :param description: Human-readable description of the Thing - :type description: str - :param version: Version number of the Thing - :type version: str - :param types: List of Thing types, used by clients to filter discovered Things - :type types: list of str - :param format_flask_exceptions: JSON format all exception responses - :type format_flask_exceptions: bool - :param external_links: Use external links in Thing Description where possible - :type external_links: bool - :param json_encoder: JSON encoder class for the app - """ - - def __init__( - self, - app=None, - id_: str = None, - prefix: str = "", - title: str = "", - description: str = "", - version: str = "0.0.0", - types: list = None, - format_flask_exceptions: bool = True, - external_links: bool = True, - json_encoder=LabThingsJSONEncoder, - ): - if id_ is None: - self.id = f"{title}:{uuid.uuid4()}".replace(" ", "") - else: - self.id = id_ - - if types is None: - types = [] - self.app = app # Becomes a Flask app - self.sockets = None # Becomes a Socket(app) websocket handler - - self.components = ( - {} - ) # Dictionary of attached component objects, available to extensions - - self.extensions = {} # Dictionary of LabThings extension objects - - self.actions = Pool() # Pool of threads for Actions - - self.events = {} # Dictionary of Event affordances - - self.views = [] # List of View classes - self._property_views = {} # Dictionary of PropertyView views - self._action_views = {} # Dictionary of ActionView views - - self.subscribers = set() # Set of connected event subscribers - - self.endpoints = set() # Set of endpoint strings - - self.url_prefix = prefix # Global URL prefix for all LabThings views - - self.types = types or [] - - self._description = description - self._title = title - self._version = version - - # Flags for error handling - self.format_flask_exceptions = format_flask_exceptions - - # Logging handler - # TODO: Add cleanup code - self.log_handler = LabThingLogger() - logging.getLogger().addHandler(self.log_handler) - - # Representation formatter map - self.representations = DEFAULT_REPRESENTATIONS - - # OpenAPI spec for Swagger docs - self.spec = APISpec( - title=self.title, - version=self.version, - openapi_version="3.0.2", - plugins=[FlaskPlugin(), FlaskLabThingsPlugin(), MarshmallowPlugin()], - ) - - # Thing description - self.thing_description = ThingDescription(external_links=external_links) - - # JSON encoder class - self.json_encoder = json_encoder - - if app is not None: - self.init_app(app) - - @property - def description(self,): - """ - Human-readable description of the Thing - """ - return self._description - - @description.setter - def description(self, description: str): - """ - Human-readable description of the Thing - :param description: str: - """ - self._description = description - self.spec.description = description - - @property - def title(self): - """ - Human-readable title of the Thing - """ - return self._title - - @title.setter - def title(self, title: str): - """ - Human-readable title of the Thing - :param description: str: - """ - self._title = title - self.spec.title = title - - @property - def safe_title(self): - """ - Lowercase title with no whitespace - """ - title = self.title - if not title: - title = "unknown" - title = title.replace(" ", "") - title = title.lower() - return title - - @property - def version(self,): - """ - Version number of the Thing - """ - return str(self._version) - - @version.setter - def version(self, version: str): - """ - Version number of the Thing - :param version: str: - """ - self._version = version - self.spec.version = version - - # Flask stuff - - def init_app(self, app): - """ - Initialize this class with the given :class:`flask.Flask` application. - :param app: the Flask application or blueprint object - - :type app: flask.Flask - :type app: flask.Blueprint - - Examples:: - labthing = LabThing() - labthing.add_view(...) - labthing.init_app(app) - """ - self.app = app - - # Register Flask extension - app.extensions = getattr(app, "extensions", {}) - app.extensions[EXTENSION_NAME] = weakref.ref(self) - - # Flask error formatter - if self.format_flask_exceptions: - error_handler = SerializedExceptionHandler() - error_handler.init_app(app) - - # Custom JSON encoder - app.json_encoder = self.json_encoder - - # Create socket handler - self.sockets = Sockets(app) - - # 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() - - # Create base sockets - self._create_base_sockets() - - # Create base events - self.add_event("logging") - - def _create_base_routes(self): - """ - Automatically add base HTTP views to the LabThing. - - Creates: - Root Thing Description - Extensions list - Legacy task list and resources - Actions queue and resources - """ - # Add root representation - self.add_view(RootView, "/", endpoint="root") - # Add thing descriptions - self.app.register_blueprint( - docs_blueprint, url_prefix=f"{self.url_prefix}/docs" - ) - self.add_root_link(SwaggerUIView, "docs") - - # Add extension overview - self.add_view(ExtensionList, "/extensions", endpoint=EXTENSION_LIST_ENDPOINT) - self.add_root_link(ExtensionList, "extensions") - # Add action routes - self.add_view(ActionQueueView, "/actions", endpoint=ACTION_LIST_ENDPOINT) - self.add_root_link(ActionQueueView, "actions") - self.add_view(ActionObjectView, "/actions/", endpoint=ACTION_ENDPOINT) - - def _create_base_sockets(self): - """ - Automatically add base WebSocket views to the LabThing. - """ - self.sockets.add_view( - self._complete_url("/ws", ""), socket_handler, endpoint="ws" - ) - self.thing_description.add_link("ws", "websocket") - - # Device stuff - - def add_component(self, component_object, component_name: str): - """ - Add a component object to the LabThing, allowing it to be - used by extensions and other views by name, rather than reference. - - :param device_object: Component object - :param device_name: str: Component name, used by extensions to find the object - - """ - self.components[component_name] = component_object - - for extension_object in self.extensions.values(): - # For each on_component function - for com_func in extension_object._on_components: - # If the component matches - if com_func.get("component", "") == component_name: - # Call the function - com_func.get("function")( - component_object, - *com_func.get("args"), - **com_func.get("kwargs"), - ) - - # Extension stuff - - def register_extension(self, extension_object): - """ - Add an extension to the LabThing. This will add API views and lifecycle - functions from the extension to the LabThing - - :param extension_object: Extension instance - :type extension_object: labthings.extensions.BaseExtension - - """ - # Type check - 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_endpoint, extension_view in extension_object.views.items(): - - # Append extension name to endpoint - endpoint = f"{extension_object.name}/{extension_view_endpoint}" - - # Add route to the extensions blueprint - self.add_view( - extension_view["view"], - *("/extensions" + url for url in extension_view["urls"]), - endpoint=endpoint, - **extension_view["kwargs"], - ) - - # For each on_register function - for reg_func in extension_object._on_registers: - # Call the function - reg_func.get("function")(*reg_func.get("args"), **reg_func.get("kwargs")) - - # For each on_component function - for com_func in extension_object._on_components: - key = com_func.get("component", "") - # If the component has already been added - if key in self.components: - # Call the function - com_func.get("function")( - self.components.get(key), - *com_func.get("args"), - **com_func.get("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 = [self.url_prefix, registration_prefix, url_part] - u = "".join(clean_url_string(part) for part in parts if part) - return u if u else "/" - - def add_view(self, view, *urls, endpoint=None, **kwargs): - """Adds a view to the api. - - :param view: View class - :type resource: :class:`labthings.views.View` - :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 kwargs: kwargs to be forwarded to the constructor - of the view. - - Additional keyword arguments not specified above will be passed as-is - to :meth:`flask.Flask.add_url_rule`. - - Examples:: - - labthing.add_view(HelloWorld, '/', '/hello') - labthing.add_view(Foo, '/foo', endpoint="foo") - labthing.add_view(FooSpecial, '/special/foo', endpoint="foo") - """ - endpoint = endpoint or camel_to_snake(view.__name__) - - logging.debug(f"{endpoint}: {type(view)} @ {urls}") - - if self.app is not None: - self._register_view(self.app, view, *urls, endpoint=endpoint, **kwargs) - - self.views.append((view, urls, endpoint, kwargs)) - - def view(self, *urls, **kwargs): - """Wraps a :class:`labthings.View` class, adding it to the LabThing. - Parameters are the same as :meth:`~labthings.LabThing.add_view`. - - Example:: - - app = Flask(__name__) - labthing = labthings.LabThing(app) - - @labthing.view('/properties/my_property') - class Foo(labthings.views.PropertyView): - schema = labthings.fields.String() - - def get(self): - return 'Hello, World!' - """ - - 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 camel_to_snake(view.__name__) - self.endpoints.add(endpoint) - resource_class_args = kwargs.pop("resource_class_args", ()) - resource_class_kwargs = kwargs.pop("resource_class_kwargs", {}) - - 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, endpoint=endpoint, **kwargs) - # Add to self.sockets so that the socket middleware may - # intercept the connection - if hasattr(view, "websocket"): - self.sockets.add_url_rule( - rule, view_func=resource_func, endpoint=endpoint - ) - - # There might be a better way to do this than _rules_by_endpoint, - # but I can't find one so this will do for now. Skipping PYL-W0212 - flask_rules = app.url_map._rules_by_endpoint.get(endpoint) # skipcq: PYL-W0212 - with app.test_request_context(): - self.spec.path(view=resource_func, interaction=view) - - # Handle resource groups listed in API spec - if issubclass(view, ActionView): - self.thing_description.action(flask_rules, view) - self._action_views[view.endpoint] = view - if issubclass(view, PropertyView): - self.thing_description.property(flask_rules, view) - self._property_views[view.endpoint] = view - - # Event stuff - def add_event(self, name, schema=None): - """ - - :param name: - :param schema: (Default value = None) - - """ - # TODO: Handle schema - # TODO: Add view for event, returning list of Event.events - self.events[name] = Event(name, schema=schema) - self.thing_description.event(self.events[name]) - - def emit(self, event_type: str, data: dict): - """Find a matching event type if one exists, and emit some data to it - - :param event_type: str: - :param data: dict: - - """ - event_object = self.events[event_type] - self.message(event_object, data) - - def message(self, event: Event, data: dict): - """Emit an event object to all subscribers - - :param event: Event: - :param data: dict: - - """ - event_response = event.emit(data) - for sub in self.subscribers: - sub.emit(event_response) - - # Utilities - - def url_for(self, view, **values): - """Generates a URL to the given resource. - Works like :func:`flask.url_for`. - - :param view: - :param values: - - """ - if isinstance(view, str): - endpoint = view - else: - endpoint = getattr(view, "endpoint", None) - if not endpoint: - return "" - # Default to external links - if "_external" not in values: - values["_external"] = True - return url_for(endpoint, **values) - - def add_root_link(self, view, rel, kwargs=None, params=None): - """ - - :param view: - :param rel: - :param kwargs: (Default value = None) - :param params: (Default value = None) - - """ - if kwargs is None: - kwargs = {} - if params is None: - params = {} - self.thing_description.add_link(view, rel, kwargs=kwargs, params=params) +import logging +import uuid +import weakref + +from apispec import APISpec +from apispec_webframeworks.flask import FlaskPlugin +from flask import url_for + +from .actions.pool import Pool +from .apispec import FlaskLabThingsPlugin, MarshmallowPlugin +from .default_views.actions import ActionObjectView, ActionQueueView +from .default_views.docs import SwaggerUIView, docs_blueprint +from .default_views.extensions import ExtensionList +from .default_views.root import RootView +from .event import Event +from .extensions import BaseExtension +from .httperrorhandler import SerializedExceptionHandler +from .json.encoder import LabThingsJSONEncoder +from .logging import LabThingLogger +from .names import ( + ACTION_ENDPOINT, + ACTION_LIST_ENDPOINT, + EXTENSION_LIST_ENDPOINT, + EXTENSION_NAME, +) +from .representations import DEFAULT_REPRESENTATIONS +from .td import ThingDescription +from .utilities import camel_to_snake, clean_url_string +from .views import ActionView, PropertyView + +# from apispec.ext.marshmallow import MarshmallowPlugin + + +class LabThing: + """ + The main entry point for the application. + You need to initialize it with a Flask Application: :: + + >>> app = Flask(__name__) + >>> labthing = labthings.LabThing(app) + + Alternatively, you can use :meth:`init_app` to set the Flask application + after it has been constructed. + + :param app: the Flask application object + :type app: flask.Flask + :param prefix: Prefix all routes with a value, eg v1 or 2010-04-01 + :type prefix: str + :param title: Human-readable title of the Thing + :type title: str + :param description: Human-readable description of the Thing + :type description: str + :param version: Version number of the Thing + :type version: str + :param types: List of Thing types, used by clients to filter discovered Things + :type types: list of str + :param format_flask_exceptions: JSON format all exception responses + :type format_flask_exceptions: bool + :param external_links: Use external links in Thing Description where possible + :type external_links: bool + :param json_encoder: JSON encoder class for the app + """ + + def __init__( + self, + app=None, + id_: str = None, + prefix: str = "", + title: str = "", + description: str = "", + version: str = "0.0.0", + types: list = None, + format_flask_exceptions: bool = True, + external_links: bool = True, + json_encoder=LabThingsJSONEncoder, + ): + if id_ is None: + self.id = f"{title}:{uuid.uuid4()}".replace(" ", "") + else: + self.id = id_ + + if types is None: + types = [] + self.app = app # Becomes a Flask app + + self.components = ( + {} + ) # Dictionary of attached component objects, available to extensions + + self.extensions = {} # Dictionary of LabThings extension objects + + self.actions = Pool() # Pool of threads for Actions + + self.events = {} # Dictionary of Event affordances + + self.views = [] # List of View classes + self._property_views = {} # Dictionary of PropertyView views + self._action_views = {} # Dictionary of ActionView views + + self.subscribers = set() # Set of connected event subscribers + + self.endpoints = set() # Set of endpoint strings + + self.url_prefix = prefix # Global URL prefix for all LabThings views + + self.types = types or [] + + self._description = description + self._title = title + self._version = version + + # Flags for error handling + self.format_flask_exceptions = format_flask_exceptions + + # Logging handler + # TODO: Add cleanup code + self.log_handler = LabThingLogger() + logging.getLogger().addHandler(self.log_handler) + + # Representation formatter map + self.representations = DEFAULT_REPRESENTATIONS + + # OpenAPI spec for Swagger docs + self.spec = APISpec( + title=self.title, + version=self.version, + openapi_version="3.0.2", + plugins=[FlaskPlugin(), FlaskLabThingsPlugin(), MarshmallowPlugin()], + ) + + # Thing description + self.thing_description = ThingDescription(external_links=external_links) + + # JSON encoder class + self.json_encoder = json_encoder + + if app is not None: + self.init_app(app) + + @property + def description(self,): + """ + Human-readable description of the Thing + """ + return self._description + + @description.setter + def description(self, description: str): + """ + Human-readable description of the Thing + :param description: str: + """ + self._description = description + self.spec.description = description + + @property + def title(self): + """ + Human-readable title of the Thing + """ + return self._title + + @title.setter + def title(self, title: str): + """ + Human-readable title of the Thing + :param description: str: + """ + self._title = title + self.spec.title = title + + @property + def safe_title(self): + """ + Lowercase title with no whitespace + """ + title = self.title + if not title: + title = "unknown" + title = title.replace(" ", "") + title = title.lower() + return title + + @property + def version(self,): + """ + Version number of the Thing + """ + return str(self._version) + + @version.setter + def version(self, version: str): + """ + Version number of the Thing + :param version: str: + """ + self._version = version + self.spec.version = version + + # Flask stuff + + def init_app(self, app): + """ + Initialize this class with the given :class:`flask.Flask` application. + :param app: the Flask application or blueprint object + + :type app: flask.Flask + :type app: flask.Blueprint + + Examples:: + labthing = LabThing() + labthing.add_view(...) + labthing.init_app(app) + """ + self.app = app + + # Register Flask extension + app.extensions = getattr(app, "extensions", {}) + app.extensions[EXTENSION_NAME] = weakref.ref(self) + + # Flask error formatter + if self.format_flask_exceptions: + error_handler = SerializedExceptionHandler() + error_handler.init_app(app) + + # Custom JSON encoder + app.json_encoder = self.json_encoder + + # 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() + + # Create base events + self.add_event("logging") + + def _create_base_routes(self): + """ + Automatically add base HTTP views to the LabThing. + + Creates: + Root Thing Description + Extensions list + Legacy task list and resources + Actions queue and resources + """ + # Add root representation + self.add_view(RootView, "/", endpoint="root") + # Add thing descriptions + self.app.register_blueprint( + docs_blueprint, url_prefix=f"{self.url_prefix}/docs" + ) + self.add_root_link(SwaggerUIView, "docs") + + # Add extension overview + self.add_view(ExtensionList, "/extensions", endpoint=EXTENSION_LIST_ENDPOINT) + self.add_root_link(ExtensionList, "extensions") + # Add action routes + self.add_view(ActionQueueView, "/actions", endpoint=ACTION_LIST_ENDPOINT) + self.add_root_link(ActionQueueView, "actions") + self.add_view(ActionObjectView, "/actions/", endpoint=ACTION_ENDPOINT) + + + # Device stuff + + def add_component(self, component_object, component_name: str): + """ + Add a component object to the LabThing, allowing it to be + used by extensions and other views by name, rather than reference. + + :param device_object: Component object + :param device_name: str: Component name, used by extensions to find the object + + """ + self.components[component_name] = component_object + + for extension_object in self.extensions.values(): + # For each on_component function + for com_func in extension_object._on_components: + # If the component matches + if com_func.get("component", "") == component_name: + # Call the function + com_func.get("function")( + component_object, + *com_func.get("args"), + **com_func.get("kwargs"), + ) + + # Extension stuff + + def register_extension(self, extension_object): + """ + Add an extension to the LabThing. This will add API views and lifecycle + functions from the extension to the LabThing + + :param extension_object: Extension instance + :type extension_object: labthings.extensions.BaseExtension + + """ + # Type check + 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_endpoint, extension_view in extension_object.views.items(): + + # Append extension name to endpoint + endpoint = f"{extension_object.name}/{extension_view_endpoint}" + + # Add route to the extensions blueprint + self.add_view( + extension_view["view"], + *("/extensions" + url for url in extension_view["urls"]), + endpoint=endpoint, + **extension_view["kwargs"], + ) + + # For each on_register function + for reg_func in extension_object._on_registers: + # Call the function + reg_func.get("function")(*reg_func.get("args"), **reg_func.get("kwargs")) + + # For each on_component function + for com_func in extension_object._on_components: + key = com_func.get("component", "") + # If the component has already been added + if key in self.components: + # Call the function + com_func.get("function")( + self.components.get(key), + *com_func.get("args"), + **com_func.get("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 = [self.url_prefix, registration_prefix, url_part] + u = "".join(clean_url_string(part) for part in parts if part) + return u if u else "/" + + def add_view(self, view, *urls, endpoint=None, **kwargs): + """Adds a view to the api. + + :param view: View class + :type resource: :class:`labthings.views.View` + :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 kwargs: kwargs to be forwarded to the constructor + of the view. + + Additional keyword arguments not specified above will be passed as-is + to :meth:`flask.Flask.add_url_rule`. + + Examples:: + + labthing.add_view(HelloWorld, '/', '/hello') + labthing.add_view(Foo, '/foo', endpoint="foo") + labthing.add_view(FooSpecial, '/special/foo', endpoint="foo") + """ + endpoint = endpoint or camel_to_snake(view.__name__) + + logging.debug(f"{endpoint}: {type(view)} @ {urls}") + + if self.app is not None: + self._register_view(self.app, view, *urls, endpoint=endpoint, **kwargs) + + self.views.append((view, urls, endpoint, kwargs)) + + def view(self, *urls, **kwargs): + """Wraps a :class:`labthings.View` class, adding it to the LabThing. + Parameters are the same as :meth:`~labthings.LabThing.add_view`. + + Example:: + + app = Flask(__name__) + labthing = labthings.LabThing(app) + + @labthing.view('/properties/my_property') + class Foo(labthings.views.PropertyView): + schema = labthings.fields.String() + + def get(self): + return 'Hello, World!' + """ + + 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 camel_to_snake(view.__name__) + self.endpoints.add(endpoint) + resource_class_args = kwargs.pop("resource_class_args", ()) + resource_class_kwargs = kwargs.pop("resource_class_kwargs", {}) + + 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, endpoint=endpoint, **kwargs) + + # There might be a better way to do this than _rules_by_endpoint, + # but I can't find one so this will do for now. Skipping PYL-W0212 + flask_rules = app.url_map._rules_by_endpoint.get(endpoint) # skipcq: PYL-W0212 + with app.test_request_context(): + self.spec.path(view=resource_func, interaction=view) + + # Handle resource groups listed in API spec + if issubclass(view, ActionView): + self.thing_description.action(flask_rules, view) + self._action_views[view.endpoint] = view + if issubclass(view, PropertyView): + self.thing_description.property(flask_rules, view) + self._property_views[view.endpoint] = view + + # Event stuff + def add_event(self, name, schema=None): + """ + + :param name: + :param schema: (Default value = None) + + """ + # TODO: Handle schema + # TODO: Add view for event, returning list of Event.events + self.events[name] = Event(name, schema=schema) + self.thing_description.event(self.events[name]) + + def emit(self, event_type: str, data: dict): + """Find a matching event type if one exists, and emit some data to it + + :param event_type: str: + :param data: dict: + + """ + event_object = self.events[event_type] + self.message(event_object, data) + + def message(self, event: Event, data: dict): + """Emit an event object to all subscribers + + :param event: Event: + :param data: dict: + + """ + event_response = event.emit(data) + for sub in self.subscribers: + sub.emit(event_response) + + # Utilities + + def url_for(self, view, **values): + """Generates a URL to the given resource. + Works like :func:`flask.url_for`. + + :param view: + :param values: + + """ + if isinstance(view, str): + endpoint = view + else: + endpoint = getattr(view, "endpoint", None) + if not endpoint: + return "" + # Default to external links + if "_external" not in values: + values["_external"] = True + return url_for(endpoint, **values) + + def add_root_link(self, view, rel, kwargs=None, params=None): + """ + + :param view: + :param rel: + :param kwargs: (Default value = None) + :param params: (Default value = None) + + """ + if kwargs is None: + kwargs = {} + if params is None: + params = {} + self.thing_description.add_link(view, rel, kwargs=kwargs, params=params) diff --git a/src/labthings/sockets.py b/src/labthings/sockets.py deleted file mode 100644 index df08698e..00000000 --- a/src/labthings/sockets.py +++ /dev/null @@ -1,18 +0,0 @@ -from .representations import encode_json - - -class SocketSubscriber: - """ """ - - def __init__(self, ws): - self.ws = ws - - def emit(self, event: dict): - """ - - :param event: dict: - - """ - response = encode_json(event) - # TODO: Logic surrounding if this subscriber is subscribed to the requested event type - self.ws.send(response) diff --git a/src/labthings/td.py b/src/labthings/td.py index 0873023b..92863f9a 100644 --- a/src/labthings/td.py +++ b/src/labthings/td.py @@ -1,261 +1,258 @@ -from flask import has_request_context, request - -from .event import Event -from .find import current_labthing -from .json.schemas import rule_to_params, rule_to_path, schema_to_json -from .utilities import ResourceURL, get_docstring, snake_to_camel -from .views import View - - -def view_to_thing_forms(rules: list, view: View, external: bool = True): - """Build a W3C form description for an general View - - :param rules: List of Flask rules - :type rules: list - :param view: View class - :type view: View - :param rules: list: - :param view: View: - :param external: bool: Use external links where possible - :returns: Form description - :rtype: [dict] - - """ - forms = [] - - # Get map from ops to HTTP methods - for op, meth in getattr(view, "_opmap", {}).items(): - if hasattr(view, meth): - prop_urls = [rule_to_path(rule) for rule in rules] - - # Get content_types - content_type = getattr(view, "content_type", "application/json") - response_content_type = getattr( - view, "response_content_type", "application/json" - ) - - for url in prop_urls: - # Basic form parameters - form = { - "op": op, - "href": ResourceURL(url, external=external), - "contentType": content_type, - } - # Optional override response content type - if response_content_type != content_type: - form["response"] = {"contentType": response_content_type} - # Fix URL for the View's websocket method - if meth.upper() == "WEBSOCKET": - form["href"] = ResourceURL(url, external=external, protocol="ws") - # Add HTTP methods for non-websocket forms - else: - form["htv:methodName"] = meth.upper() - form["href"] = ResourceURL(url, external=external) - - forms.append(form) - - return forms - - -class ThingDescription: - """ """ - - def __init__(self, external_links: bool = True): - # Public attributes - self.properties = {} - self.actions = {} - self.events = {} - - # Private attributes - self._links = [] - - # Settings - self.external_links = external_links - - # Init - super().__init__() - - @property - def links(self): - """ """ - td_links = [] - for link_description in self._links: - td_links.append( - { - "rel": link_description.get("rel"), - "href": current_labthing().url_for( - link_description.get("view"), - **link_description.get("params"), - _external=self.external_links, - ), - **link_description.get("kwargs"), - } - ) - return td_links - - def add_link(self, view, rel, kwargs=None, params=None): - """ - - :param view: - :param rel: - :param kwargs: (Default value = None) - :param params: (Default value = None) - - """ - if kwargs is None: - kwargs = {} - if params is None: - params = {} - self._links.append( - {"rel": rel, "view": view, "params": params, "kwargs": kwargs} - ) - - def to_dict(self): - """ """ - td = { - "@context": [ - "https://www.w3.org/2019/wot/td/v1", - "https://iot.mozilla.org/schemas/", - ], - "@type": current_labthing().types, - "id": current_labthing().id, - "title": current_labthing().title, - "description": current_labthing().description, - "properties": self.properties, - "actions": self.actions, - # "events": self.events, # TODO: Enable once properly populated - "links": self.links, - "securityDefinitions": {"nosec_sc": {"scheme": "nosec"}}, - "security": "nosec_sc", - } - - if not self.external_links and has_request_context(): - td["base"] = request.host_url - - return td - - def view_to_thing_property(self, rules: list, view: View): - """ - - :param rules: list: - :param view: View: - - """ - prop_urls = [rule_to_path(rule) for rule in rules] - - # Basic description - prop_description = { - "title": getattr(view, "title", None) or view.__name__, - "description": getattr(view, "description", None) or get_docstring(view), - "readOnly": not ( - hasattr(view, "post") or hasattr(view, "put") or hasattr(view, "delete") - ), - "writeOnly": not hasattr(view, "get"), - "forms": view_to_thing_forms(rules, view, external=self.external_links), - "uriVariables": {}, - } - - semtype = getattr(view, "semtype", None) - if semtype: - prop_description["@type"] = semtype - - # Look for a _propertySchema in the Property classes API SPec - prop_schema = getattr(view, "schema", None) - - if prop_schema: - # Convert schema to JSON - prop_schema_json = schema_to_json(prop_schema) - - # Add schema to prop description - prop_description.update(prop_schema_json) - - # Add URI variables - for prop_rule in rules: - params_dict = {} - for param in rule_to_params(prop_rule): - params_dict.update( - { - param.get("name"): { - "type": param.get("type") or param.get("schema").get("type") - } - } - ) - prop_description["uriVariables"].update(params_dict) - if not prop_description["uriVariables"]: - del prop_description["uriVariables"] - - return prop_description - - def view_to_thing_action(self, rules: list, view: View): - """ - - :param rules: list: - :param view: View: - - """ - action_urls = [rule_to_path(rule) for rule in rules] - - # Basic description - action_description = { - "title": getattr(view, "title", None) or view.__name__, - "description": getattr(view, "description", None) or get_docstring(view), - "safe": getattr(view, "safe", False), - "idempotent": getattr(view, "idempotent", False), - "forms": view_to_thing_forms(rules, view, external=self.external_links), - } - - # Look for a _params in the Action classes API Spec - action_input_schema = getattr(view, "args", None) - if action_input_schema: - # Add schema to prop description - action_description["input"] = schema_to_json(action_input_schema) - - semtype = getattr(view, "semtype", None) - if semtype: - action_description["@type"] = semtype - - # Look for a _schema in the Action classes API Spec - action_output_schema = getattr(view, "schema", None) - if action_output_schema: - # Add schema to prop description - action_description["output"] = schema_to_json(action_output_schema) - - return action_description - - def property(self, rules: list, view: View): - """ - - :param rules: list: - :param view: View: - - """ - endpoint = getattr(view, "endpoint", None) or getattr(rules[0], "endpoint") - key = snake_to_camel(endpoint) - self.properties[key] = self.view_to_thing_property(rules, view) - - def action(self, rules: list, view: View): - """Add a view representing an Action. - - NB at present this will fail for any view that doesn't support POST - requests. - - :param rules: list: - :param view: View: - - """ - if not hasattr(view, "post"): - raise AttributeError( - f"The API View '{view}' was added as an Action, \ - but it does not have a POST method." - ) - endpoint = getattr(view, "endpoint", None) or getattr(rules[0], "endpoint") - key = snake_to_camel(endpoint) - self.actions[key] = self.view_to_thing_action(rules, view) - - def event(self, event: Event): - """ - - :param event: Event: - - """ - print("Swalling as Events aren't yet implemented in the Thing Description") +from flask import has_request_context, request + +from .event import Event +from .find import current_labthing +from .json.schemas import rule_to_params, rule_to_path, schema_to_json +from .utilities import ResourceURL, get_docstring, snake_to_camel +from .views import View + + +def view_to_thing_forms(rules: list, view: View, external: bool = True): + """Build a W3C form description for an general View + + :param rules: List of Flask rules + :type rules: list + :param view: View class + :type view: View + :param rules: list: + :param view: View: + :param external: bool: Use external links where possible + :returns: Form description + :rtype: [dict] + + """ + forms = [] + + # Get map from ops to HTTP methods + for op, meth in getattr(view, "_opmap", {}).items(): + if hasattr(view, meth): + prop_urls = [rule_to_path(rule) for rule in rules] + + # Get content_types + content_type = getattr(view, "content_type", "application/json") + response_content_type = getattr( + view, "response_content_type", "application/json" + ) + + for url in prop_urls: + # Basic form parameters + form = { + "op": op, + "href": ResourceURL(url, external=external), + "contentType": content_type, + } + # Optional override response content type + if response_content_type != content_type: + form["response"] = {"contentType": response_content_type} + # Add HTTP methods + else: + form["htv:methodName"] = meth.upper() + form["href"] = ResourceURL(url, external=external) + + forms.append(form) + + return forms + + +class ThingDescription: + """ """ + + def __init__(self, external_links: bool = True): + # Public attributes + self.properties = {} + self.actions = {} + self.events = {} + + # Private attributes + self._links = [] + + # Settings + self.external_links = external_links + + # Init + super().__init__() + + @property + def links(self): + """ """ + td_links = [] + for link_description in self._links: + td_links.append( + { + "rel": link_description.get("rel"), + "href": current_labthing().url_for( + link_description.get("view"), + **link_description.get("params"), + _external=self.external_links, + ), + **link_description.get("kwargs"), + } + ) + return td_links + + def add_link(self, view, rel, kwargs=None, params=None): + """ + + :param view: + :param rel: + :param kwargs: (Default value = None) + :param params: (Default value = None) + + """ + if kwargs is None: + kwargs = {} + if params is None: + params = {} + self._links.append( + {"rel": rel, "view": view, "params": params, "kwargs": kwargs} + ) + + def to_dict(self): + """ """ + td = { + "@context": [ + "https://www.w3.org/2019/wot/td/v1", + "https://iot.mozilla.org/schemas/", + ], + "@type": current_labthing().types, + "id": current_labthing().id, + "title": current_labthing().title, + "description": current_labthing().description, + "properties": self.properties, + "actions": self.actions, + # "events": self.events, # TODO: Enable once properly populated + "links": self.links, + "securityDefinitions": {"nosec_sc": {"scheme": "nosec"}}, + "security": "nosec_sc", + } + + if not self.external_links and has_request_context(): + td["base"] = request.host_url + + return td + + def view_to_thing_property(self, rules: list, view: View): + """ + + :param rules: list: + :param view: View: + + """ + prop_urls = [rule_to_path(rule) for rule in rules] + + # Basic description + prop_description = { + "title": getattr(view, "title", None) or view.__name__, + "description": getattr(view, "description", None) or get_docstring(view), + "readOnly": not ( + hasattr(view, "post") or hasattr(view, "put") or hasattr(view, "delete") + ), + "writeOnly": not hasattr(view, "get"), + "forms": view_to_thing_forms(rules, view, external=self.external_links), + "uriVariables": {}, + } + + semtype = getattr(view, "semtype", None) + if semtype: + prop_description["@type"] = semtype + + # Look for a _propertySchema in the Property classes API SPec + prop_schema = getattr(view, "schema", None) + + if prop_schema: + # Convert schema to JSON + prop_schema_json = schema_to_json(prop_schema) + + # Add schema to prop description + prop_description.update(prop_schema_json) + + # Add URI variables + for prop_rule in rules: + params_dict = {} + for param in rule_to_params(prop_rule): + params_dict.update( + { + param.get("name"): { + "type": param.get("type") or param.get("schema").get("type") + } + } + ) + prop_description["uriVariables"].update(params_dict) + if not prop_description["uriVariables"]: + del prop_description["uriVariables"] + + return prop_description + + def view_to_thing_action(self, rules: list, view: View): + """ + + :param rules: list: + :param view: View: + + """ + action_urls = [rule_to_path(rule) for rule in rules] + + # Basic description + action_description = { + "title": getattr(view, "title", None) or view.__name__, + "description": getattr(view, "description", None) or get_docstring(view), + "safe": getattr(view, "safe", False), + "idempotent": getattr(view, "idempotent", False), + "forms": view_to_thing_forms(rules, view, external=self.external_links), + } + + # Look for a _params in the Action classes API Spec + action_input_schema = getattr(view, "args", None) + if action_input_schema: + # Add schema to prop description + action_description["input"] = schema_to_json(action_input_schema) + + semtype = getattr(view, "semtype", None) + if semtype: + action_description["@type"] = semtype + + # Look for a _schema in the Action classes API Spec + action_output_schema = getattr(view, "schema", None) + if action_output_schema: + # Add schema to prop description + action_description["output"] = schema_to_json(action_output_schema) + + return action_description + + def property(self, rules: list, view: View): + """ + + :param rules: list: + :param view: View: + + """ + endpoint = getattr(view, "endpoint", None) or getattr(rules[0], "endpoint") + key = snake_to_camel(endpoint) + self.properties[key] = self.view_to_thing_property(rules, view) + + def action(self, rules: list, view: View): + """Add a view representing an Action. + + NB at present this will fail for any view that doesn't support POST + requests. + + :param rules: list: + :param view: View: + + """ + if not hasattr(view, "post"): + raise AttributeError( + f"The API View '{view}' was added as an Action, \ + but it does not have a POST method." + ) + endpoint = getattr(view, "endpoint", None) or getattr(rules[0], "endpoint") + key = snake_to_camel(endpoint) + self.actions[key] = self.view_to_thing_action(rules, view) + + def event(self, event: Event): + """ + + :param event: Event: + + """ + print("Swalling as Events aren't yet implemented in the Thing Description") diff --git a/src/labthings/views/__init__.py b/src/labthings/views/__init__.py index f1deae70..7372b920 100644 --- a/src/labthings/views/__init__.py +++ b/src/labthings/views/__init__.py @@ -1,261 +1,253 @@ -from collections import OrderedDict - -from flask import abort, request -from flask.views import MethodView -from werkzeug.exceptions import BadRequest -from werkzeug.wrappers import Response as ResponseBase - -from ..actions.pool import Pool -from ..deque import Deque -from ..event import PropertyStatusEvent -from ..find import current_labthing -from ..marshalling import marshal_with, use_args -from ..representations import DEFAULT_REPRESENTATIONS -from ..schema import ActionSchema, Schema, build_action_schema -from ..utilities import unpack -from . import builder, op - -__all__ = ["MethodView", "View", "ActionView", "PropertyView", "op", "builder"] - - -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. - - """ - - endpoint = None # Store the View endpoint for use in specs - - # Basic view spec metadata - tags: list = [] # Custom tags the user can add - title: None - - # Internal - _cls_tags = set() # Class tags that shouldn't be removed - _opmap = {} # Mapping of Thing Description ops to class methods - - def __init__(self, *args, **kwargs): - MethodView.__init__(self, *args, **kwargs) - - # Set the default representations - self.representations = ( - current_labthing().representations - if current_labthing() - else DEFAULT_REPRESENTATIONS - ) - - @classmethod - def get_tags(cls): - """ """ - return cls._cls_tags.union(set(cls.tags)) - - def get_value(self): - """ """ - get_method = getattr(self, "get", None) # Look for this views GET method - if get_method is None: - return None - if not callable(get_method): - raise TypeError("Attribute 'get' of View must be a callable") - response = get_method() # pylint: disable=not-callable - if isinstance(response, ResponseBase): # Pluck useful data out of HTTP response - return response.json if response.json else response.data - else: # Unless somehow an HTTP response isn't returned... - return response - - def _find_request_method(self): - meth = getattr(self, request.method.lower(), None) - if meth is None and request.method == "HEAD": - meth = getattr(self, "get", None) - - # Handle the case of a GET request asking for WS upgrade where - # no websocket method is defined on the view - if request.method == "GET" and request.environ.get("wsgi.websocket"): - ws_meth = getattr(self, "websocket", None) - if ws_meth is None: - abort(400, "Unable to upgrade websocket connection") - return ws_meth - - return meth - - def dispatch_request(self, *args, **kwargs): - """ - - :param *args: - :param **kwargs: - - """ - meth = self._find_request_method() - - # Generate basic response - return self.represent_response(meth(*args, **kwargs)) - - def represent_response(self, response): - """Take the marshalled return value of a function - and build a representation response - - :param response: - - """ - if isinstance(response, ResponseBase): # There may be a better way to test - return response - - representations = self.representations or OrderedDict() - - # noinspection PyUnresolvedReferences - mediatype = request.accept_mimetypes.best_match(representations, default=None) - if mediatype in representations: - data, code, headers = unpack(response) - response = representations[mediatype](data, code, headers) - response.headers["Content-Type"] = mediatype - return response - return response - - -class ActionView(View): - """ """ - - # Data formatting - schema: Schema = None # Schema for Action response - args: dict = None # Schema for input arguments - semtype: str = None # Semantic type string - - # Spec overrides - content_type = "application/json" # Input contentType - response_content_type = "application/json" # Output contentType - responses = {} # Custom responses for invokeaction - - # Spec parameters - safe: bool = False # Does the action complete WITHOUT changing the Thing state - idempotent: bool = False # Can the action be performed idempotently - - # Action handling - wait_for: int = 1 # Time in seconds to wait before returning the action as pending/running - default_stop_timeout: int = None # Time in seconds to wait for the action thread to end after a stop request before terminating it forcefully - - # Internal - _opmap = { - "invokeaction": "post" - } # Mapping of Thing Description ops to class methods - _cls_tags = {"actions"} - _deque = Deque() # Action queue - _emergency_pool = Pool() - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - @classmethod - def get(cls): - """ - Default method for GET requests. Returns the action queue (including already finished actions) for this action - """ - queue_schema = build_action_schema(cls.schema, cls.args)(many=True) - return queue_schema.dump(cls._deque) - - def dispatch_request(self, *args, **kwargs): - """ - - :param *args: - :param **kwargs: - - """ - meth = self._find_request_method() - - # Let base View handle non-POST requests - if request.method != "POST": - return View.dispatch_request(self, *args, **kwargs) - - # Inject request arguments if an args schema is defined - if self.args: - meth = use_args(self.args)(meth) - - # Marhal response if a response schema is defined - if self.schema: - meth = marshal_with(self.schema)(meth) - - # Try to find a pool on the current LabThing, but fall back to Views emergency pool - pool = ( - current_labthing().actions if current_labthing() else self._emergency_pool - ) - # Make a task out of the views `post` method - task = pool.spawn(meth, *args, **kwargs) - # Optionally override the threads default_stop_timeout - if self.default_stop_timeout is not None: - task.default_stop_timeout = self.default_stop_timeout - - # Keep a copy of the raw, unmarshalled JSON input in the task - try: - task.input = request.json - except BadRequest: - task.input = None - - # Wait up to 2 second for the action to complete or error - try: - task.get(block=True, timeout=self.wait_for) - except TimeoutError: - pass - - # Log the action to the view's deque - self._deque.append(task) - - # If the action returns quickly, and returns a valid Response, return it as-is - if task.output and isinstance(task.output, ResponseBase): - return self.represent_response(task.output, 200) - - return self.represent_response((ActionSchema().dump(task), 201)) - - -class PropertyView(View): - """ """ - - # Data formatting - schema: Schema = None # Schema for input AND output - semtype: str = None # Semantic type string - - # Spec overrides - content_type = "application/json" # Input and output contentType - responses = {} # Custom responses for all interactions - - # Internal - _opmap = { - "readproperty": "get", - "writeproperty": "put", - } # Mapping of Thing Description ops to class methods - _cls_tags = {"properties"} - - def dispatch_request(self, *args, **kwargs): - """ - - :param *args: - :param **kwargs: - - """ - meth = self._find_request_method() - - # POST and PUT methods can be used to write properties - # In all other cases, ignore arguments - if request.method in ("PUT", "POST") and self.schema: - meth = use_args(self.schema)(meth) - - # All methods should serialise properties - if self.schema: - meth = marshal_with(self.schema)(meth) - - # Generate basic response - resp = self.represent_response(meth(*args, **kwargs)) - - # Emit property event - if request.method in ("POST", "PUT"): - property_value = self.get_value() - property_name = getattr(self, "endpoint", None) or getattr( - self, "__name__", "unknown" - ) - - if current_labthing(): - current_labthing().message( - PropertyStatusEvent(property_name), property_value, - ) - - return resp +from collections import OrderedDict + +from flask import abort, request +from flask.views import MethodView +from werkzeug.exceptions import BadRequest +from werkzeug.wrappers import Response as ResponseBase + +from ..actions.pool import Pool +from ..deque import Deque +from ..event import PropertyStatusEvent +from ..find import current_labthing +from ..marshalling import marshal_with, use_args +from ..representations import DEFAULT_REPRESENTATIONS +from ..schema import ActionSchema, Schema, build_action_schema +from ..utilities import unpack +from . import builder, op + +__all__ = ["MethodView", "View", "ActionView", "PropertyView", "op", "builder"] + + +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. + + """ + + endpoint = None # Store the View endpoint for use in specs + + # Basic view spec metadata + tags: list = [] # Custom tags the user can add + title: None + + # Internal + _cls_tags = set() # Class tags that shouldn't be removed + _opmap = {} # Mapping of Thing Description ops to class methods + + def __init__(self, *args, **kwargs): + MethodView.__init__(self, *args, **kwargs) + + # Set the default representations + self.representations = ( + current_labthing().representations + if current_labthing() + else DEFAULT_REPRESENTATIONS + ) + + @classmethod + def get_tags(cls): + """ """ + return cls._cls_tags.union(set(cls.tags)) + + def get_value(self): + """ """ + get_method = getattr(self, "get", None) # Look for this views GET method + if get_method is None: + return None + if not callable(get_method): + raise TypeError("Attribute 'get' of View must be a callable") + response = get_method() # pylint: disable=not-callable + if isinstance(response, ResponseBase): # Pluck useful data out of HTTP response + return response.json if response.json else response.data + else: # Unless somehow an HTTP response isn't returned... + return response + + def _find_request_method(self): + meth = getattr(self, request.method.lower(), None) + if meth is None and request.method == "HEAD": + meth = getattr(self, "get", None) + + return meth + + def dispatch_request(self, *args, **kwargs): + """ + + :param *args: + :param **kwargs: + + """ + meth = self._find_request_method() + + # Generate basic response + return self.represent_response(meth(*args, **kwargs)) + + def represent_response(self, response): + """Take the marshalled return value of a function + and build a representation response + + :param response: + + """ + if isinstance(response, ResponseBase): # There may be a better way to test + return response + + representations = self.representations or OrderedDict() + + # noinspection PyUnresolvedReferences + mediatype = request.accept_mimetypes.best_match(representations, default=None) + if mediatype in representations: + data, code, headers = unpack(response) + response = representations[mediatype](data, code, headers) + response.headers["Content-Type"] = mediatype + return response + return response + + +class ActionView(View): + """ """ + + # Data formatting + schema: Schema = None # Schema for Action response + args: dict = None # Schema for input arguments + semtype: str = None # Semantic type string + + # Spec overrides + content_type = "application/json" # Input contentType + response_content_type = "application/json" # Output contentType + responses = {} # Custom responses for invokeaction + + # Spec parameters + safe: bool = False # Does the action complete WITHOUT changing the Thing state + idempotent: bool = False # Can the action be performed idempotently + + # Action handling + wait_for: int = 1 # Time in seconds to wait before returning the action as pending/running + default_stop_timeout: int = None # Time in seconds to wait for the action thread to end after a stop request before terminating it forcefully + + # Internal + _opmap = { + "invokeaction": "post" + } # Mapping of Thing Description ops to class methods + _cls_tags = {"actions"} + _deque = Deque() # Action queue + _emergency_pool = Pool() + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + @classmethod + def get(cls): + """ + Default method for GET requests. Returns the action queue (including already finished actions) for this action + """ + queue_schema = build_action_schema(cls.schema, cls.args)(many=True) + return queue_schema.dump(cls._deque) + + def dispatch_request(self, *args, **kwargs): + """ + + :param *args: + :param **kwargs: + + """ + meth = self._find_request_method() + + # Let base View handle non-POST requests + if request.method != "POST": + return View.dispatch_request(self, *args, **kwargs) + + # Inject request arguments if an args schema is defined + if self.args: + meth = use_args(self.args)(meth) + + # Marhal response if a response schema is defined + if self.schema: + meth = marshal_with(self.schema)(meth) + + # Try to find a pool on the current LabThing, but fall back to Views emergency pool + pool = ( + current_labthing().actions if current_labthing() else self._emergency_pool + ) + # Make a task out of the views `post` method + task = pool.spawn(meth, *args, **kwargs) + # Optionally override the threads default_stop_timeout + if self.default_stop_timeout is not None: + task.default_stop_timeout = self.default_stop_timeout + + # Keep a copy of the raw, unmarshalled JSON input in the task + try: + task.input = request.json + except BadRequest: + task.input = None + + # Wait up to 2 second for the action to complete or error + try: + task.get(block=True, timeout=self.wait_for) + except TimeoutError: + pass + + # Log the action to the view's deque + self._deque.append(task) + + # If the action returns quickly, and returns a valid Response, return it as-is + if task.output and isinstance(task.output, ResponseBase): + return self.represent_response(task.output, 200) + + return self.represent_response((ActionSchema().dump(task), 201)) + + +class PropertyView(View): + """ """ + + # Data formatting + schema: Schema = None # Schema for input AND output + semtype: str = None # Semantic type string + + # Spec overrides + content_type = "application/json" # Input and output contentType + responses = {} # Custom responses for all interactions + + # Internal + _opmap = { + "readproperty": "get", + "writeproperty": "put", + } # Mapping of Thing Description ops to class methods + _cls_tags = {"properties"} + + def dispatch_request(self, *args, **kwargs): + """ + + :param *args: + :param **kwargs: + + """ + meth = self._find_request_method() + + # POST and PUT methods can be used to write properties + # In all other cases, ignore arguments + if request.method in ("PUT", "POST") and self.schema: + meth = use_args(self.schema)(meth) + + # All methods should serialise properties + if self.schema: + meth = marshal_with(self.schema)(meth) + + # Generate basic response + resp = self.represent_response(meth(*args, **kwargs)) + + # Emit property event + if request.method in ("POST", "PUT"): + property_value = self.get_value() + property_name = getattr(self, "endpoint", None) or getattr( + self, "__name__", "unknown" + ) + + if current_labthing(): + current_labthing().message( + PropertyStatusEvent(property_name), property_value, + ) + + return resp diff --git a/src/labthings/wsgi.py b/src/labthings/wsgi.py index e0ffc63a..60b598b7 100644 --- a/src/labthings/wsgi.py +++ b/src/labthings/wsgi.py @@ -1,153 +1,127 @@ -import logging -import signal -import socket -import threading - -from flask_threaded_sockets import ThreadedWebsocketServer -from werkzeug.debug import DebuggedApplication -from zeroconf import IPVersion, ServiceInfo, Zeroconf, get_all_addresses - -from .find import current_labthing - -sentinel = object() - - -class Server: - """Combined WSGI+WebSocket+mDNS server. - - :param host: Host IP address. Defaults to 0.0.0.0. - :type host: string - :param port: Host port. Defaults to 7485. - :type port: int - :param debug: Enable server debug mode. Defaults to False. - :type debug: bool - :param zeroconf: Enable the zeroconf (mDNS) server. Defaults to True. - :type zeroconf: bool - """ - - def __init__( - self, app, host="0.0.0.0", port=7485, debug=False, zeroconf=True, **kwargs - ): - self.app = app - # Find LabThing attached to app - self.labthing = current_labthing(app) - - # Server properties - self.host = host - self.port = port - self.debug = debug - self.zeroconf = zeroconf - - # Servers - self.wsgi_server = None - self.zeroconf_server = None - self.service_info = None - self.service_infos = [] - - # Events - self.started = threading.Event() - - def _register_zeroconf(self): - if self.labthing: - # Get list of host addresses - mdns_addresses = { - socket.inet_aton(i) - for i in get_all_addresses() - if i not in ("127.0.0.1", "0.0.0.0") - } - # LabThing service - self.service_infos.append( - ServiceInfo( - "_labthing._tcp.local.", - f"{self.labthing.safe_title}._labthing._tcp.local.", - port=self.port, - properties={ - "path": self.labthing.url_prefix, - "id": self.labthing.id, - }, - addresses=mdns_addresses, - ) - ) - self.zeroconf_server = Zeroconf(ip_version=IPVersion.V4Only) - for service in self.service_infos: - self.zeroconf_server.register_service(service) - - def stop(self): - """Stop the server and unregister mDNS records""" - # Unregister zeroconf service - if self.zeroconf_server: - logging.info("Unregistering zeroconf services") - for service in self.service_infos: - self.zeroconf_server.unregister_service(service) - self.zeroconf_server.close() - # Stop WSGI server with timeout - if self.wsgi_server: - logging.info("Shutting down WSGI server") - self.wsgi_server.stop(timeout=5) - # Clear started event - if self.started.is_set(): - self.started.clear() - logging.info("Done") - - def start(self): - """Start the server and register mDNS records""" - # Unmodified version of app - app_to_run = self.app - # Handle zeroconf - if self.zeroconf: - self._register_zeroconf() - - # Handle debug mode - if self.debug: - app_to_run = DebuggedApplication(self.app) - logging.getLogger("werkzeug").setLevel(logging.DEBUG) - logging.getLogger("zeroconf").setLevel(logging.DEBUG) - - # Slightly more useful logger output - friendlyhost = "localhost" if self.host == "0.0.0.0" else self.host - print("Starting LabThings WSGI Server") - print(f"Debug mode: {self.debug}") - print(f"Running on http://{friendlyhost}:{self.port} (Press CTRL+C to quit)") - - # Create WSGIServer - self.wsgi_server = ThreadedWebsocketServer(self.host, self.port, app_to_run) - - # Serve - signal.signal(signal.SIGTERM, self.stop) - - # Set started event - self.started.set() - try: - self.wsgi_server.serve_forever() - except (KeyboardInterrupt, SystemExit): # pragma: no cover - logging.warning( - "Terminating by KeyboardInterrupt or SystemExit" - ) # pragma: no cover - self.stop() # pragma: no cover - - def run(self, host=None, port=None, debug=None, zeroconf=None, **kwargs): - """Starts the server allowing for runtime parameters. Designed to immitate - the old Flask app.run style of starting an app - - :param host: Host IP address. Defaults to 0.0.0.0. - :type host: string - :param port: Host port. Defaults to 7485. - :type port: int - :param debug: Enable server debug mode. Defaults to False. - :type debug: bool - :param zeroconf: Enable the zeroconf (mDNS) server. Defaults to True. - :type zeroconf: bool - """ - if port is not None: - self.port = int(port) - - if host is not None: - self.host = str(host) - - if debug is not None: - self.debug = debug - - if zeroconf is not None: - self.zeroconf = zeroconf - - self.start() +import logging +import signal +import socket +import threading + +from werkzeug.serving import run_simple +from werkzeug.debug import DebuggedApplication +from zeroconf import IPVersion, ServiceInfo, Zeroconf, get_all_addresses + +from .find import current_labthing + +sentinel = object() + + +class Server: + """Combined WSGI+mDNS server. + + :param host: Host IP address. Defaults to 0.0.0.0. + :type host: string + :param port: Host port. Defaults to 7485. + :type port: int + :param debug: Enable server debug mode. Defaults to False. + :type debug: bool + :param zeroconf: Enable the zeroconf (mDNS) server. Defaults to True. + :type zeroconf: bool + """ + + def __init__( + self, app, host="0.0.0.0", port=7485, debug=False, zeroconf=True, **kwargs + ): + self.app = app + # Find LabThing attached to app + self.labthing = current_labthing(app) + + # Server properties + self.host = host + self.port = port + self.debug = debug + self.zeroconf = zeroconf + + # Servers + self.wsgi_server = None + self.zeroconf_server = None + self.service_info = None + self.service_infos = [] + + def _register_zeroconf(self): + if self.labthing: + # Get list of host addresses + mdns_addresses = { + socket.inet_aton(i) + for i in get_all_addresses() + if i not in ("127.0.0.1", "0.0.0.0") + } + # LabThing service + self.service_infos.append( + ServiceInfo( + "_labthing._tcp.local.", + f"{self.labthing.safe_title}._labthing._tcp.local.", + port=self.port, + properties={ + "path": self.labthing.url_prefix, + "id": self.labthing.id, + }, + addresses=mdns_addresses, + ) + ) + self.zeroconf_server = Zeroconf(ip_version=IPVersion.V4Only) + for service in self.service_infos: + self.zeroconf_server.register_service(service) + + def start(self): + """Start the server and register mDNS records""" + # Handle zeroconf + if self.zeroconf: + self._register_zeroconf() + + # Slightly more useful logger output + friendlyhost = "localhost" if self.host == "0.0.0.0" else self.host + print("Starting LabThings WSGI Server") + print(f"Debug mode: {self.debug}") + print(f"Running on http://{friendlyhost}:{self.port} (Press CTRL+C to quit)") + + # Create WSGIServer + run_simple(self.host, self.port, self.app, use_debugger=self.debug, threaded=True, processes=1) + + # When server stops + if self.zeroconf_server: + logging.info("Unregistering zeroconf services") + for service in self.service_infos: + self.zeroconf_server.unregister_service(service) + self.zeroconf_server.close() + # Stop WSGI server with timeout + if self.wsgi_server: + logging.info("Shutting down WSGI server") + self.wsgi_server.stop(timeout=5) + # Clear started event + if self.started.is_set(): + self.started.clear() + logging.info("Done") + + def run(self, host=None, port=None, debug=None, zeroconf=None, **kwargs): + """Starts the server allowing for runtime parameters. Designed to immitate + the old Flask app.run style of starting an app + + :param host: Host IP address. Defaults to 0.0.0.0. + :type host: string + :param port: Host port. Defaults to 7485. + :type port: int + :param debug: Enable server debug mode. Defaults to False. + :type debug: bool + :param zeroconf: Enable the zeroconf (mDNS) server. Defaults to True. + :type zeroconf: bool + """ + if port is not None: + self.port = int(port) + + if host is not None: + self.host = str(host) + + if debug is not None: + self.debug = debug + + if zeroconf is not None: + self.zeroconf = zeroconf + + self.start() diff --git a/tests/conftest.py b/tests/conftest.py index 85a52d0d..00ba5e29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,349 +1,260 @@ -import json -import os - -import jsonschema -import pytest -from apispec import APISpec -from apispec.ext.marshmallow import MarshmallowPlugin -from flask import Flask -from flask.testing import FlaskClient -from flask.views import MethodView -from werkzeug.test import EnvironBuilder - -from labthings import LabThing -from labthings.actions import Pool -from labthings.json import encode_json -from labthings.views import ActionView, PropertyView, View - - -class Helpers: - @staticmethod - def validate_thing_description(thing_description, app_ctx, schemas_path): - schema = json.load(open(os.path.join(schemas_path, "w3c_td_schema.json"), "r")) - jsonschema.Draft7Validator.check_schema(schema) - - # Build a TD dictionary - with app_ctx.test_request_context(): - td_dict = thing_description.to_dict() - - # Allow our LabThingsJSONEncoder to encode the RD - td_json = encode_json(td_dict) - # Decode the JSON back into a primitive dictionary - td_json_dict = json.loads(td_json) - # Validate - jsonschema.validate(instance=td_json_dict, schema=schema) - - -@pytest.fixture -def helpers(): - return Helpers - - -class FakeWebsocket: - def __init__(self, message: str, recieve_once=True, close_after=None): - self.message = message - self.responses = [] - self.closed = False - self.recieve_once = recieve_once - - self.close_after = close_after or [] - - # I mean screw whoever is responsible for this having to be a thing... - self.receive = self.recieve - - def recieve(self): - # Get message - message_to_send = self.message - # If only sending a message to the server once - if self.recieve_once: - # Clear our message - self.message = None - return message_to_send - - @property - def response(self): - if len(self.responses) >= 1: - return self.responses[-1] - else: - return None - - def send(self, response): - self.responses.append(response) - # Close WS after getting the pre-defined unit test response - if response in self.close_after: - self.closed = True - return response - - -class JsonClient(FlaskClient): - def open(self, *args, **kwargs): - kwargs.setdefault( - "headers", - {"Content-Type": "application/json", "Accept": "application/json"}, - ) - kwargs.setdefault("content_type", "application/json") - return super().open(*args, **kwargs) - - -class SocketClient(FlaskClient): - def __init__(self, app, response_wrapper, *args, **kwargs): - super().__init__(app, response_wrapper, *args, **kwargs) - self.app = app - self.response_wrapper = response_wrapper - self.socket = FakeWebsocket(message=None) - self.environ_base = { - "HTTP_UPGRADE": "websocket", - "wsgi.websocket": self.socket, - } - - def connect(self, *args, message=None, **kwargs): - kwargs.setdefault("environ_overrides", {})[ - "flask._preserve_context" - ] = self.preserve_context - kwargs.setdefault("environ_base", self.environ_base) - builder = EnvironBuilder(*args, **kwargs) - - try: - environ = builder.get_environ() - finally: - builder.close() - - self.socket.message = message - - with self.app.app_context(): - run_wsgi_app(self.app, environ) - - # Once the connection has been closed, return responses - return self.socket.responses - - -def run_wsgi_app(app, environ, buffered=False): - response = [] - buffer = [] - - def start_response(status, headers, exc_info=None): - if exc_info: - try: - raise exc_info[1].with_traceback(exc_info[2]) - finally: - exc_info = None - response[:] = [status, headers] - return buffer.append - - # Return value from the wsgi_app call - # In the case of our SocketMiddleware, will return [] - app_rv = app(environ, start_response) - return app_rv - - -@pytest.fixture -def empty_view_cls(): - class EmptyViewClass(View): - def get(self): - pass - - def post(self): - pass - - def put(self): - pass - - def delete(self): - pass - - return EmptyViewClass - - -@pytest.fixture -def flask_view_cls(): - class ViewClass(MethodView): - def get(self): - return "GET" - - def post(self): - return "POST" - - def put(self): - return "PUT" - - def delete(self): - return "DELETE" - - return ViewClass - - -@pytest.fixture -def view_cls(): - class ViewClass(View): - def get(self): - return "GET" - - def post(self): - return "POST" - - def put(self): - return "PUT" - - def delete(self): - return "DELETE" - - return ViewClass - - -@pytest.fixture -def action_view_cls(): - class ActionViewClass(ActionView): - def post(self): - return "POST" - - return ActionViewClass - - -@pytest.fixture -def property_view_cls(): - class PropertyViewClass(PropertyView): - def get(self): - return "GET" - - def put(self): - return "PUT" - - return PropertyViewClass - - -@pytest.fixture -def spec(): - return APISpec( - title="Python-LabThings PyTest", - version="1.0.0", - openapi_version="3.0.2", - plugins=[MarshmallowPlugin()], - ) - - -@pytest.fixture() -def app(request): - - app = Flask(__name__) - app.config["TESTING"] = True - - # pushes an application context manually - ctx = app.app_context() - ctx.push() - - # bind the test life with the context through the - request.addfinalizer(ctx.pop) - return app - - -@pytest.fixture -def thing(app): - thing = LabThing(app, external_links=False) - with app.app_context(): - return thing - - -@pytest.fixture() -def thing_ctx(thing): - with thing.app.app_context(): - yield thing.app - - -@pytest.fixture() -def debug_app(request): - - app = Flask(__name__) - app.config["TESTING"] = True - app.debug = True - - # pushes an application context manually - ctx = app.app_context() - ctx.push() - - # bind the test life with the context through the - request.addfinalizer(ctx.pop) - return app - - -@pytest.fixture() -def app_ctx(app): - with app.app_context(): - yield app - - -@pytest.fixture() -def app_ctx_debug(debug_app): - with debug_app.app_context(): - yield debug_app - - -@pytest.fixture -def req_ctx(app): - with app.test_request_context() as ctx: - yield ctx - - -@pytest.fixture -def client(app): - app.test_client_class = JsonClient - return app.test_client() - - -@pytest.fixture -def debug_client(debug_app): - debug_app.test_client_class = JsonClient - return debug_app.test_client() - - -@pytest.fixture -def text_client(app): - return app.test_client() - - -@pytest.fixture -def ws_client(app): - app.test_client_class = SocketClient - return app.test_client() - - -@pytest.fixture -def thing_client(thing): - thing.app.test_client_class = JsonClient - return thing.app.test_client() - - -@pytest.fixture -def static_path(app): - return os.path.join(os.path.dirname(__file__), "static") - - -@pytest.fixture -def schemas_path(app): - return os.path.join(os.path.dirname(__file__), "schemas") - - -@pytest.fixture -def extensions_path(app): - return os.path.join(os.path.dirname(__file__), "extensions") - - -@pytest.fixture -def fake_websocket(): - """ - Return a fake websocket client - that sends a given message, waits for a response, then closes - """ - - def _foo(*args, **kwargs): - return FakeWebsocket(*args, **kwargs) - - return _foo - - -@pytest.fixture -def task_pool(): - """ - Return a task pool - """ - - return Pool() +import json +import os + +import jsonschema +import pytest +from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin +from flask import Flask +from flask.testing import FlaskClient +from flask.views import MethodView +from werkzeug.test import EnvironBuilder + +from labthings import LabThing +from labthings.actions import Pool +from labthings.json import encode_json +from labthings.views import ActionView, PropertyView, View + + +class Helpers: + @staticmethod + def validate_thing_description(thing_description, app_ctx, schemas_path): + schema = json.load(open(os.path.join(schemas_path, "w3c_td_schema.json"), "r")) + jsonschema.Draft7Validator.check_schema(schema) + + # Build a TD dictionary + with app_ctx.test_request_context(): + td_dict = thing_description.to_dict() + + # Allow our LabThingsJSONEncoder to encode the RD + td_json = encode_json(td_dict) + # Decode the JSON back into a primitive dictionary + td_json_dict = json.loads(td_json) + # Validate + jsonschema.validate(instance=td_json_dict, schema=schema) + + +@pytest.fixture +def helpers(): + return Helpers + +class JsonClient(FlaskClient): + def open(self, *args, **kwargs): + kwargs.setdefault( + "headers", + {"Content-Type": "application/json", "Accept": "application/json"}, + ) + kwargs.setdefault("content_type", "application/json") + return super().open(*args, **kwargs) + + +def run_wsgi_app(app, environ, buffered=False): + response = [] + buffer = [] + + def start_response(status, headers, exc_info=None): + if exc_info: + try: + raise exc_info[1].with_traceback(exc_info[2]) + finally: + exc_info = None + response[:] = [status, headers] + return buffer.append + + # Return value from the wsgi_app call + app_rv = app(environ, start_response) + return app_rv + + +@pytest.fixture +def empty_view_cls(): + class EmptyViewClass(View): + def get(self): + pass + + def post(self): + pass + + def put(self): + pass + + def delete(self): + pass + + return EmptyViewClass + + +@pytest.fixture +def flask_view_cls(): + class ViewClass(MethodView): + def get(self): + return "GET" + + def post(self): + return "POST" + + def put(self): + return "PUT" + + def delete(self): + return "DELETE" + + return ViewClass + + +@pytest.fixture +def view_cls(): + class ViewClass(View): + def get(self): + return "GET" + + def post(self): + return "POST" + + def put(self): + return "PUT" + + def delete(self): + return "DELETE" + + return ViewClass + + +@pytest.fixture +def action_view_cls(): + class ActionViewClass(ActionView): + def post(self): + return "POST" + + return ActionViewClass + + +@pytest.fixture +def property_view_cls(): + class PropertyViewClass(PropertyView): + def get(self): + return "GET" + + def put(self): + return "PUT" + + return PropertyViewClass + + +@pytest.fixture +def spec(): + return APISpec( + title="Python-LabThings PyTest", + version="1.0.0", + openapi_version="3.0.2", + plugins=[MarshmallowPlugin()], + ) + + +@pytest.fixture() +def app(request): + + app = Flask(__name__) + app.config["TESTING"] = True + + # pushes an application context manually + ctx = app.app_context() + ctx.push() + + # bind the test life with the context through the + request.addfinalizer(ctx.pop) + return app + + +@pytest.fixture +def thing(app): + thing = LabThing(app, external_links=False) + with app.app_context(): + return thing + + +@pytest.fixture() +def thing_ctx(thing): + with thing.app.app_context(): + yield thing.app + + +@pytest.fixture() +def debug_app(request): + + app = Flask(__name__) + app.config["TESTING"] = True + app.debug = True + + # pushes an application context manually + ctx = app.app_context() + ctx.push() + + # bind the test life with the context through the + request.addfinalizer(ctx.pop) + return app + + +@pytest.fixture() +def app_ctx(app): + with app.app_context(): + yield app + + +@pytest.fixture() +def app_ctx_debug(debug_app): + with debug_app.app_context(): + yield debug_app + + +@pytest.fixture +def req_ctx(app): + with app.test_request_context() as ctx: + yield ctx + + +@pytest.fixture +def client(app): + app.test_client_class = JsonClient + return app.test_client() + + +@pytest.fixture +def debug_client(debug_app): + debug_app.test_client_class = JsonClient + return debug_app.test_client() + + +@pytest.fixture +def text_client(app): + return app.test_client() + + +@pytest.fixture +def thing_client(thing): + thing.app.test_client_class = JsonClient + return thing.app.test_client() + + +@pytest.fixture +def static_path(app): + return os.path.join(os.path.dirname(__file__), "static") + + +@pytest.fixture +def schemas_path(app): + return os.path.join(os.path.dirname(__file__), "schemas") + + +@pytest.fixture +def extensions_path(app): + return os.path.join(os.path.dirname(__file__), "extensions") + + +@pytest.fixture +def task_pool(): + """ + Return a task pool + """ + + return Pool() diff --git a/tests/test_default_views_socket_handler.py b/tests/test_default_views_socket_handler.py deleted file mode 100644 index b7c20bbb..00000000 --- a/tests/test_default_views_socket_handler.py +++ /dev/null @@ -1,16 +0,0 @@ -from labthings.default_views.sockets import process_socket_message, socket_handler - - -def test_socket_handler(thing_ctx, fake_websocket): - with thing_ctx.test_request_context(): - ws = fake_websocket( - "__unittest", recieve_once=True, close_after=["__unittest_response"] - ) - socket_handler(ws) - assert "__unittest_response" in ws.responses - - -### Will need regular updating as new message handlers are added -def test_process_socket_message(): - assert process_socket_message("message") is None - assert process_socket_message(None) is None