diff --git a/openflexure_microscope/api/v1/__init__.py b/.gitattributes similarity index 100% rename from openflexure_microscope/api/v1/__init__.py rename to .gitattributes diff --git a/.gitignore b/.gitignore index a4651afe..ab3442c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Pyenv files .python-version -.venv/ +.venv*/ +.env*/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -25,6 +26,7 @@ var/ *.egg-info/ .installed.cfg *.egg +pip-wheel-metadata/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..379984f0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,68 @@ +stages: + - build + - deploy + + +# Cache modules in between jobs +cache: + key: ${CI_COMMIT_REF_SLUG} + paths: + - node_modules/ + + +# Electron app build +build: + stage: build + image: nikolaik/python-nodejs:python3.7-nodejs14 + + before_script: + - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python + + script: + # Install server + - $HOME/.poetry/bin/poetry install + # Run static build script + - $HOME/.poetry/bin/poetry run build_static + # Build distribution archive + - mkdir -p dist + - tar -c --exclude-vcs --exclude-from .tarignore -vzf dist/openflexure-microscope-server-${CI_COMMIT_REF_NAME}.tar.gz . + + artifacts: + name: "dist" + expire_in: 1 week + paths: + - "./dist/*" + + only: + - merge_requests + - tags + - web + + +# Deploy to builds.openflexure.org +deploy: + stage: deploy + dependencies: + - build + image: ubuntu:latest + + before_script: + - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + - eval $(ssh-agent -s) + - ssh-add <(echo "$SSH_PRIVATE_KEY_BATH_OPENFLEXURE_BASE64" | base64 --decode) + - mkdir -p ~/.ssh + - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + + script: + # Install rsync if not already installed + - 'which rsync || ( apt-get update -y && apt-get install rsync -y )' + + # Upload the builds folder to openflexure-microscope builds + - rsync -hrvz -e ssh dist/ ci-user@openflexure.bath.ac.uk:/var/www/build/openflexure-microscope-server + + # Run update-latest.py on the build server + - ssh -t ci-user@openflexure.bath.ac.uk "/var/www/build/update-latest.py" + + only: + - tags + - web diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md new file mode 100644 index 00000000..e6e38299 --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,33 @@ +# Bug report + +## Summary + +(Summarize the bug encountered concisely) + +## Configuration + +I'm using: + +**Camera:** (E.g. Raspberry Pi camera v2) + +**Motor controller:** (E.g. Sangaboard, or Arduino Nano) + +## Steps to reproduce + +(How one can reproduce the issue - this is very important) + +## Relevant logs and/or screenshots + +To access your microscope log, either: + +* Run `ofm log` on your microscope, and copy/paste the output here + +* Go to `http://:5000/log`, download the log file, and attach it here using the "Attach a File" button below + * In most setups, `http://microscope.local:5000/log` should work + +## Additional details + +(Anything else you think might be relevant to mention) + +/label ~bug +/cc @jtc42 diff --git a/.gitlab/issue_templates/Feature request.md b/.gitlab/issue_templates/Feature request.md new file mode 100644 index 00000000..02e47af5 --- /dev/null +++ b/.gitlab/issue_templates/Feature request.md @@ -0,0 +1,20 @@ +# Feature request + +## Summary + +(Summarize the bug encountered concisely) + +## Configuration + +I'm using: + +**Camera:** (E.g. Raspberry Pi camera v2) + +**Motor controller:** (E.g. Sangaboard, or Arduino Nano) + +## Additional details + +(Anything else you think might be relevant to mention) + +/label ~feature +/cc @jtc42 diff --git a/.tarignore b/.tarignore new file mode 100644 index 00000000..708114f4 --- /dev/null +++ b/.tarignore @@ -0,0 +1,80 @@ +Distribution archive rules +node_modules +.gitlab-ci.yml +./dist +!openflexure_microscope/api/static/dist +.tarignore + +# Pyenv files +.python-version +.venv* +.env* + +# Byte-compiled / optimized / DLL files +__pycache__ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Pytho +env/ +build +develop-eggs +downloads +eggs +lib +lib64 +parts +sdist +var +*.egg-info +.installed.cfg +*.egg +pip-wheel-metadata + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov +.tox +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build + +# PyBuilder +target + +#Big-o files +capture +record +*.data + +#IDE files +.vscode +.idea +activate + +# UML diagrams +classes.png +packages.png diff --git a/README.md b/README.md index acb4fd60..85164355 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -OpenFlexure Microscope Software -===================== +# OpenFlexure Microscope Software -# Quickstart +## Quickstart A general user-guide on setting up your microscope can be found [**here on our website**](https://www.openflexure.org/projects/microscope/). This includes basic installation instructions suitable for most users. @@ -9,19 +8,44 @@ This includes basic installation instructions suitable for most users. Full developer documentation can be found on [**ReadTheDocs**](https://openflexure-microscope-software.readthedocs.io/en/stable/). This includes installing the server in a mode better suited for active development. +## Installation + +* `git clone https://gitlab.com/openflexure/openflexure-microscope-server.git` +* (If running on a microscope Pi) `ofm activate` +* `poetry install` +* `poetry run build_static` + +### Node installation + +* Note, building the static interface will require a valid Node.js installation +* To build on a Raspberry Pi: + * `curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -` + * `sudo apt install nodejs` + +### Distributing + +* `mkdir -p dist` +* `tar -c --exclude-vcs --exclude-from .tarignore -vzf dist/openflexure-microscope-server.tar.gz .` + ## Build-system + As of 1.0.0b0, we're using [Poetry](https://github.com/sdispater/poetry) to manage dependencies, build, and distribute the package. All package information and dependencies are found in `pyproject.toml`, in line with [PEP 518](https://www.python.org/dev/peps/pep-0518/). If you're developing this package, make use of `poetry.lock` to ensure you're using the latest locked dependency list. ## Microscope plugins + The Microscope module, and Flask app, both support plugins for extending lower-level functionality not well suited to web API calls. This plugin system is still in fairly early development, and is not yet properly documented. The current documentation can be found [here](https://openflexure-microscope-software.readthedocs.io/en/latest/plugins.html). ## Running tests through the PTVSD remote debugger (port 3000) -- From the openflexure-microscope-software directory, run `python3 -m ptvsd --host 0.0.0.0 --port 3000 --wait tests/test_camera.py` +* From the openflexure-microscope-software directory, run `python3 -m ptvsd --host 0.0.0.0 --port 3000 --wait tests/test_camera.py` + +## Credits + +### Piexif -# Credits -## Piexif The microscope server includes a forked copy of hMatoba's [Piexif](https://github.com/hMatoba/Piexif), licensed under the MIT License. -## Video streaming + +### Video streaming + Based on supporting code for the article [video streaming with Flask](http://blog.miguelgrinberg.com/post/video-streaming-with-flask) and its follow-up [Flask Video Streaming Revisited](http://blog.miguelgrinberg.com/post/flask-video-streaming-revisited). diff --git a/docs/Makefile b/docs/Makefile index 69fe55ec..5d25534e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -a -E SPHINXBUILD = sphinx-build SOURCEDIR = source BUILDDIR = build diff --git a/docs/make.bat b/docs/make.bat index 4d9eb83d..b713a4e8 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -10,6 +10,8 @@ if "%SPHINXBUILD%" == "" ( set SOURCEDIR=source set BUILDDIR=build +set SPHINXOPTS="-E" + if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL diff --git a/docs/source/api.rst b/docs/source/api.rst index 218713b7..4fd4b616 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,25 +1,9 @@ HTTP API ======== -Summary -------- -.. qrefflask:: openflexure_microscope.api.app:app - :undoc-endpoints: index - :undoc-static: - :endpoints: +Live documentation +------------------ -Details -------- -.. autoflask:: openflexure_microscope.api.app:app - :undoc-endpoints: index - :undoc-static: - :endpoints: - :order: path +Full, interactive Swagger documentation for your microscopes web API is available from the microscope itself. From any browser, go to ``http://{your microscope IP address}/api/v2/docs/swagger-ui``. -Example Requests ----------------- - -.. toctree:: - :maxdepth: 2 - - ./apirequests.rst \ No newline at end of file +.. note:: We should have an online copy of the API SwaggerUI documentation up soon. \ No newline at end of file diff --git a/docs/source/apirequests.rst b/docs/source/apirequests.rst deleted file mode 100644 index 2c836c15..00000000 --- a/docs/source/apirequests.rst +++ /dev/null @@ -1,108 +0,0 @@ -Introduction ------------- - -In these examples, our microscope is located at the IP ``192.168.1.126``, running on port 5000. - - -New Image Capture ------------------ - -Temprary capture, using video port. - -CURL -++++ -.. code-block:: none - - curl -X POST -H "Content-Type: application/json" -d \\ - '{"temporary": true, "use_video_port": true}' http://192.168.1.126:5000/api/v1/camera/capture - -HTTPie -++++++ -.. code-block:: none - - http POST http://192.168.1.126:5000/api/v1/camera/capture temporary:=true use_video_port:=true - -Python Requests -+++++++++++++++ -.. code-block:: python - - json_payload = { - "keep_on_disk": keep_on_disk, - "use_video_port": use_video_port - } - requests.post('http://192.168.1.126:5000/api/v1/camera/capture', json=json_payload) - -Update Config -------------- - -Example, changing picamera shutter speed to 2000, and the JPEG quality to 90. - -CURL -++++ -.. code-block:: none - - curl -X POST -H "Content-Type: application/json" -d \\ - '{"camera_settings": {"picamera_settings": {"shutter_speed": 2000}, "jpeg_quality": 90}}' http://192.168.1.126:5000/api/v1/config - -HTTPie -++++++ -.. code-block:: none - - http POST http://192.168.1.126:5000/api/v1/config camera_settings:='{"jpeg_quality": 90, "picamera_settings": {"shutter_speed": 2000}}' - -Python Requests -+++++++++++++++ -.. code-block:: python - - json_payload = { - camera_settings: { - "jpeg_quality": 90, - "picamera_settings": {"shutter_speed": 2000} - } - } - requests.post('http://192.168.1.126:5000/api/v1/config', json=json_payload) - -Read Config ------------ - -CURL -++++ -.. code-block:: none - - curl http://192.168.1.126:5000/api/v1/config - -HTTPie -++++++ -.. code-block:: none - - http GET http://192.168.1.126:5000/api/v1/config - -Python Requests -+++++++++++++++ -.. code-block:: python - - requests.get('http://192.168.1.126:5000/api/v1/config') - -Recalibrate Picamera --------------------- - -Starts the automatic recalibration plugin - -CURL -++++ -.. code-block:: none - - curl -X POST -H "Content-Type: application/json" \\ - http://192.168.1.126:5000/api/v1/plugin/default/camera_calibration/recalibrate - -HTTPie -++++++ -.. code-block:: none - - http POST http://192.168.1.126:5000/api/v1/plugin/default/camera_calibration/recalibrate - -Python Requests -+++++++++++++++ -.. code-block:: python - - requests.post('http://192.168.1.126:5000/api/v1/plugin/default/camera_calibration/recalibrate') \ No newline at end of file diff --git a/docs/source/basestage.rst b/docs/source/basestage.rst new file mode 100644 index 00000000..6e0fed22 --- /dev/null +++ b/docs/source/basestage.rst @@ -0,0 +1,5 @@ +Base Microscope Stage +===================== + +.. automodule:: openflexure_microscope.stage.base + :members: \ No newline at end of file diff --git a/docs/source/camera.rst b/docs/source/camera.rst index 0f36f53c..af10f99d 100644 --- a/docs/source/camera.rst +++ b/docs/source/camera.rst @@ -1,9 +1,35 @@ Camera Class -======================================================= +============ .. toctree:: :maxdepth: 2 picamera.rst basecamera.rst - capture.rst \ No newline at end of file + capture.rst + +Capturing from a camera object +------------------------------ + +In the cases of both a Raspberry Pi Streaming Camera, and a Mock Camera (attached if no real camera can be found), the camera's ``capture`` method takes as it's first positional argument either a string describing a file path to save to, or any Python file-like object. + +The :class:`openflexure_microscope.camera.capture.CaptureObject` class works by providing a file path string, but adds additional functionality around storing and retreiving EXIF metadata in compatible files. + +If, for your application, you do not require this functionality, you can pass a simple string or file-like object. For example, to take an image that will be stored in-memory, processed rapidly, and then discarded, you could use a BytesIO stream: + +.. code-block:: python + + import io + from PIL import Image + ... + + with microscope.camera.lock, io.BytesIO() as stream: + + microscope.camera.capture( + stream, + use_video_port=True, + bayer=False, + ) + + stream.seek(0) + image = Image.open(stream) \ No newline at end of file diff --git a/docs/source/capture.rst b/docs/source/capture.rst index 4b0477cc..a2d0846f 100644 --- a/docs/source/capture.rst +++ b/docs/source/capture.rst @@ -1,11 +1,11 @@ Capture Object ======================================================= -By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.camera.capture.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory. +By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.captures.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory. -The class also includes some convenience features such as automatically cleaning up captures marked as temporary, handling metadata tags and file names, and generating image thumbnails. Often, the most recent capture wants to be stored in memory, for applications that involve immediately processing the capture data in some way. Rather than manually keep track of the location of the capture data, you can use the CaptureObject ``data`` attribute, which automatically returns capture data from the fastest available location. Additionally, the class handles storing capture metadata to Exif tags in supported formats. +The class also includes some convenience features such as handling metadata tags and file names, and generating image thumbnails. Additionally, the class handles storing capture metadata to Exif tags in supported formats. Below are details of available methods and attributes. -.. automodule:: openflexure_microscope.camera.capture +.. automodule:: openflexure_microscope.captures.capture :members: \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 49b86271..cf5293a7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,32 +17,34 @@ import sys # Load module from relative imports -module_path = os.path.abspath('../..') +module_path = os.path.abspath("../..") sys.path.insert(0, module_path) # Handle mock imports for non-platform-agnostic modules from unittest.mock import MagicMock + class Mock(MagicMock): @classmethod def __getattr__(cls, name): return MagicMock() -mock_imports = ['picamera', 'picamera.array', 'picamera.mmalobj'] + +mock_imports = ["picamera", "picamera.array", "picamera.mmalobj"] sys.modules.update((mod_name, Mock()) for mod_name in mock_imports) # -- Project information ----------------------------------------------------- -project = 'OpenFlexure Microscope Software' -copyright = '2018, Bath Open Instrumentation Group' -author = 'Bath Open Instrumentation Group' +project = "OpenFlexure Microscope Software" +copyright = "2018, Bath Open Instrumentation Group" +author = "Bath Open Instrumentation Group" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" # -- General configuration --------------------------------------------------- @@ -55,32 +57,32 @@ release = '' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx.ext.ifconfig', - 'sphinxcontrib.httpdomain', - 'sphinxcontrib.autohttp.flask', - 'sphinxcontrib.autohttp.flaskqref' + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.ifconfig", + "sphinxcontrib.httpdomain", + "sphinxcontrib.autohttp.flask", + "sphinxcontrib.autohttp.flaskqref", ] # Override ordering -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -114,7 +116,7 @@ html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -130,7 +132,7 @@ html_static_path = ['_static'] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'OpenFlexureMicroscopeSoftwaredoc' +htmlhelp_basename = "OpenFlexureMicroscopeSoftwaredoc" # -- Options for LaTeX output ------------------------------------------------ @@ -139,15 +141,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -157,8 +156,13 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'OpenFlexureMicroscopeSoftware.tex', 'OpenFlexure Microscope Software Documentation', - 'Bath Open Instrumentation Group', 'manual'), + ( + master_doc, + "OpenFlexureMicroscopeSoftware.tex", + "OpenFlexure Microscope Software Documentation", + "Bath Open Instrumentation Group", + "manual", + ) ] @@ -167,8 +171,13 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'openflexuremicroscopesoftware', 'OpenFlexure Microscope Software Documentation', - [author], 1) + ( + master_doc, + "openflexuremicroscopesoftware", + "OpenFlexure Microscope Software Documentation", + [author], + 1, + ) ] @@ -178,9 +187,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'OpenFlexureMicroscopeSoftware', 'OpenFlexure Microscope Software Documentation', - author, 'OpenFlexureMicroscopeSoftware', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "OpenFlexureMicroscopeSoftware", + "OpenFlexure Microscope Software Documentation", + author, + "OpenFlexureMicroscopeSoftware", + "One line description of project.", + "Miscellaneous", + ) ] @@ -199,7 +214,7 @@ epub_title = project # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- @@ -208,11 +223,13 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'openflexure_stage': ('https://openflexure-stage.readthedocs.io/en/latest/', None), - 'picamera': ('https://picamera.readthedocs.io/en/release-1.13/', None) - } + "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), + "picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None), + "marshmallow": ("https://marshmallow.readthedocs.io/en/stable/", None), + "webargs": ("https://webargs.readthedocs.io/en/latest/", None), +} # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True \ No newline at end of file +todo_include_todos = True diff --git a/docs/source/config.rst b/docs/source/config.rst index 65640270..f2d8400d 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -1,5 +1,5 @@ -Microscope configuration -======================== +Microscope settings +=================== .. toctree:: :maxdepth: 2 @@ -7,65 +7,15 @@ Microscope configuration .. _MicroscopeRC: -Microscope RC file ------------------- +Microscope settings file +------------------------ -Microscope configuration is made persistent via a microscope runtime-config (RC) file. By default, this -file exists at ``~/.openflexure/microscope_settings.yaml``, and contains basic parameters to set up the microscope. +Microscope settings are made persistent via a microscope settings file. By default, this +file exists at ``~/.openflexure/microscope_settings.json``. -Additionally, by default, configurations for specific pieces of hardware (i.e. the stage and camera) are located -in separate "auxillary" config files, and are linked together by adding the config file's path to your main -microscope RC file. +The class :class:`openflexure_microscope.config.OpenflexureSettingsFile` provides functionality for loading a JSON-format settings file as a Python dictionary, and merging changed settings back into the file. +The default settings are loaded by the :attr:`openflexure_microscope.config.user_settings`, which can be imported anywhere in the microscope server application to allow reading and writing of persistent settings. -Loading the runtime-config -++++++++++++++++++++++++++ - -By default, a microscope object will load the a runtime-config from the default location. -This RC can then be passed to any hardware attached to the microscope. This can be particularly -useful when creating a dummy microscope object before attaching hardware. For example: - -.. code-block:: python - - from openflexure_microscope import Microscope - from openflexure_microscope.camera.pi import StreamingCamera - from openflexure_stage import OpenFlexureStage - - # Create an empty microscope - api_microscope = Microscope(None, None) - - # Create a picamera StreamingCamera, using our config - camera_obj = StreamingCamera(config=api_microscope.rc.read()) - - # Create an OpenFlexure Stage attached to /dev/ttyUSB0 - stage_obj = OpenFlexureStage("/dev/ttyUSB0") - - # Attach devices to the Microscope object - api_microscope.attach(camera_obj, stage_obj) - -Alternatively, a config can be loaded prior to creating a microscope, allowing the microscope -hardware to be created and attached in a single line. For example: - -.. code-block:: python - - from openflexure_microscope.config import OpenflexureConfig - - from openflexure_microscope import Microscope - from openflexure_microscope.camera.pi import StreamingCamera - from openflexure_stage import OpenFlexureStage - - # Create a runtime-config from the default location - rc = OpenflexureConfig(expand=True) - - # Create a populated microscope - api_microscope = Microscope( - StreamingCamera(config=rc), - OpenFlexureStage("/dev/ttyUSB0"), - config=rc - ) - - -Config module and OpenflexureConfig class ------------------------------------------ .. automodule:: openflexure_microscope.config :members: \ No newline at end of file diff --git a/docs/source/extensions/actions.rst b/docs/source/extensions/actions.rst new file mode 100644 index 00000000..5463ccfd --- /dev/null +++ b/docs/source/extensions/actions.rst @@ -0,0 +1,54 @@ +Thing Actions +============= + +Introduction +------------ + +As well as properties, the OpenFlexure Microscope Server also supports Thing Actions. +Thing Actions "invoke a function of the Thing, which manipulates state (e.g., toggling a lamp on or off) or triggers a process on the Thing (e.g., dim a lamp over time)." For the microscope, this would include moving the stage or taking a capture. Both of these require internal logic, and cannot be performed by changing a simple property. + +Actions should be *triggered* with POST requests *only*. Ideally, a view corresponding to an action should only support POST requests. + +Like properties, we use a special view class to identify a view as an action: ``ActionView``. For example, a view to perform a "quick-capture" action may look like: + +.. code-block:: python + + class QuickCaptureAPI(ActionView): + """ + Take an image capture and return it without saving + """ + # Expect a "use_video_port" boolean, which defaults to True if none is given + args = {"use_video_port": fields.Boolean(missing=True)} + + # Our success response (200) returns an image (image/jpeg mimetype) + responses = { + 200: {"content_type": "image/jpeg"} + } + + def post(self, args): + """ + Take a non-persistant image capture. + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Open a BytesIO stream to be destroyed once request has returned + with io.BytesIO() as stream: + + # Capture to our stream object + microscope.camera.capture(stream, use_video_port=args.get("use_video_port")) + + # Rewind the stream + stream.seek(0) + + # Return our image data using Flasks send_file function + return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg") + +In this example, we are also making use of the ``responses`` attribute, to document that our successful response (HTTP code 200) will return data with a mimetype ``image/jpeg``, as well as ``args`` to accept optional parameters with POST requests. + +Complete example +---------------- + +Adding this new view into our example extension, we now have: + +.. literalinclude:: ./example_extension/05_actions.py diff --git a/docs/source/plugins/schema.rst b/docs/source/extensions/ev_gui.rst similarity index 54% rename from docs/source/plugins/schema.rst rename to docs/source/extensions/ev_gui.rst index ef0375f4..79ca7a9d 100644 --- a/docs/source/plugins/schema.rst +++ b/docs/source/extensions/ev_gui.rst @@ -1,53 +1,45 @@ -Adding a GUI -===================== +OpenFlexure eV GUI +================== Introduction ------------ -In order to bind user-interface elements to your plugin, an ``api_schema`` variable must be included in your microscope plugin, similar to your ``api_views``. +The main client application for the OpenFlexure Microscope, OpenFlexure eV, can render simple GUIs (graphical user interfaces) for extensions. -This variable must be in the form of a dictionary, with all data able to be parsed into JSON. Because of this requirement, it is suggested that you create your API schema as a JSON file, and load that file as a dictionary into your plugin. For example: +We define our user interface by making use of the extensions general metadata, added using the ``add_meta`` function. This function adds arbitrary additional data to your extensions web API description, for example: .. code-block:: python - import json + # Create your extension object + my_extension = BaseExtension("com.myname.myextension", version="0.0.0") - HERE = os.path.dirname(os.path.realpath(__file__)) # Find the full path of your plugin Python file - SCHEMA_PATH = os.path.join(HERE, "schema.json") # Find the full path of the adjascent JSON file + ... - class MyPluginClass(MicroscopePlugin): + my_extension.add_meta("myKey", "My metadata value") - with open(SCHEMA_PATH, 'r') as sc: # Open the JSON file - api_schema = json.load(sc) # Load the JSON file into the api_schema dictionary - -Throughout this documentation, all example of ``api_schema`` sections will be in JSON format. Keep in mind however that it is possible to directly define your ``api_schema`` as a dictionary, without loading an external file. - - -Forms from ``api_schema`` -------------------------- - -The ``api_schema`` object essentially describes HTML forms, which it is up to the client to render. The form is constructed by specifying a set of components, and their values. A form can update it's values by sending a GET request to the API route bound to that form, and can send it's current values via a POST request to *this same API route*. +OpenFlexure eV will recognise the ``gui`` metadata key, and render properly structured descriptions of a GUI in the format described below. The ``gui`` data essentially describes HTML forms, which it is up to the client to render. The form is constructed by specifying a set of components, and their values. Each component in the form has a ``name`` property, which must match up to a property your API route expects in JSON POST requests, and returns in JSON GET requests. -Structure of ``api_schema`` + +Structure of ``gui`` --------------------------- Root level ++++++++++ -The root of your ``api_schema`` expects 3 properties: - -``id`` - A unique ID to give your client-side plugin +The root of your ``gui`` dictionary expects 2 properties: ``icon`` - The name of a Material Design icon to use for your plugin +``viewPanel`` *(optional)* - Content to display to the right of the extension form. Either ``stream`` (default), ``gallery``, or ``settings``. + ``forms`` - An array of forms as described below Form level ++++++++++ -Your plugin can contain multiple forms. For example, if your plugin creates several API routes, you will need a separate form for each route. +Your extension can contain multiple forms. For example, if your extension creates several API routes, you will need a separate form for each route. Each form is described by a JSON object, with the following properties: @@ -55,23 +47,23 @@ Each form is described by a JSON object, with the following properties: ``route`` - String of the corresponding API route. *Must* match a route defined in your ``api_views`` dictionary -``selfUpdate`` *(optional)* - Whether the client should automatically update form component values with a GET request to your API route - ``isTask`` *(optional)* - Whether the client should treat your API route as a long-running task +``isCollapsible`` *(optional)* - Whether the form can be collapsed into an accordion + ``submitLabel`` *(optional)* - String to place inside of the form's submit button -``schema`` - An array of form components as described below +``schema`` - List of dictionaries. Each dictionary element describes a form component. + +``emitOnResponse`` *(optional)* - OpenFlexure eV event to emit when a response is recieved from the extension (generally avoid unless you know you need this.) Component level +++++++++++++++ -Each form can (and probably should) contain multiple components. For example, if your API route expects several properties in POST requests, each property can be bound to a form component. +Each form can (and probably should) contain multiple components. For example, if your API route expects several parameters in a POST requests, each parameter can be bound to a form component. Upon form submission, the form data will be converted into a JSON object of key-value pairs, where the key is the components ``name``, and the value is it's current value. -On load, the form will make a GET request to it's API route, an expects a JSON object of key-value pairs in the same format (keys will be matched to component names). - An overview of available components, and their properties, can be found below. Arranging components @@ -179,14 +171,53 @@ Overview of components - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/textInput.png -JSON form example ------------------ +**Note:** Basic input types (``textInput``, ``numberInput``) can also include additional attributes for HTML input elements inputs (e.g. ``placeholder``, ``required``, ``min``, ``max``). These additional attributes will be forwarded to the rendered HTML elements. -.. literalinclude:: schema_example.json - :language: JSON +Building the GUI +---------------- + +Once you have a dictionary describing your GUI, use the :py:meth:`openflexure_microscope.api.utilities.gui.build_gui` function to fill in and expand any information required to have it properly function. This function expands your ``route`` values to include your extensions full URI, and handles returning dynamic GUIs. + +For example: + +.. code-block:: python + + my_gui = {...} + + # Create your extension object + my_extension = BaseExtension("com.myname.myextension", version="0.0.0") + + ... + + my_extension.add_meta("gui", build_gui(my_gui, my_extension)) + +Dynamic GUIs +------------ + +Instead of passing a static dictionary to :py:meth:`openflexure_microscope.api.utilities.gui.build_gui`, you can instead pass a callable function which returns a dictionary. This function is then called every time a client requests a description of active extensions. + +Using a callable has the advantage of allowing your extensions GUI to be updated as it is used. This could be as simple as changing ``value`` parameters of components (to show up-to-date default form values), but could be used to entirely change the GUI form as it is used, for example dynamically changing options in select boxes. + +For example, this could take the form: + +.. code-block:: python + + def create_dynamic_form(): + ... + generated_form_dict = {...} + return generated_form_dict + + # Create your extension object + my_extension = BaseExtension("com.myname.myextension", version="0.0.0") + + ... + + my_extension.add_meta("gui", build_gui(create_dynamic_form, my_extension)) -.. toctree:: - :maxdepth: 1 +Complete example +---------------- - ./schema/json_schema.rst \ No newline at end of file +Adding a GUI to our previous timelapse example extension becomes: + +.. literalinclude:: ./example_extension/07_ev_gui.py diff --git a/docs/source/extensions/example_extension/01_basic_structure.py b/docs/source/extensions/example_extension/01_basic_structure.py new file mode 100644 index 00000000..1ee64700 --- /dev/null +++ b/docs/source/extensions/example_extension/01_basic_structure.py @@ -0,0 +1,36 @@ +from labthings.extensions import BaseExtension +from labthings import find_component + + +def identify(): + """ + Demonstrate access to Microscope.camera, and Microscope.stage + """ + microscope = find_component("org.openflexure.microscope") + + response = ( + f"My name is {microscope.name}. " + f"My parent camera is {microscope.camera}, " + f"and my parent stage is {microscope.stage}." + ) + + return response + + +def rename(new_name): + """ + Rename the microscope + """ + + microscope = find_component("org.openflexure.microscope") + + microscope.name = new_name + microscope.save_settings() + + +# Create your extension object +my_extension = BaseExtension("com.myname.myextension", version="0.0.0") + +# Add methods to your extension +my_extension.add_method(identify, "identify") +my_extension.add_method(rename, "rename") diff --git a/docs/source/extensions/example_extension/01b_basic_structure_subclass.py b/docs/source/extensions/example_extension/01b_basic_structure_subclass.py new file mode 100644 index 00000000..20ed6b0d --- /dev/null +++ b/docs/source/extensions/example_extension/01b_basic_structure_subclass.py @@ -0,0 +1,41 @@ +from labthings.extensions import BaseExtension +from labthings import find_component + + +# Create the extension class +class MyExtension(BaseExtension): + def __init__(self): + + # Create some instance variable + self.state_variable = "An example of a persistant instance variable" + + # Superclass init function + super().__init__("com.myname.myextension", version="0.0.0") + + def identify(self): + """ + Demonstrate access to Microscope.camera, and Microscope.stage + """ + microscope = find_component("org.openflexure.microscope") + + response = ( + f"My name is {microscope.name}. " + f"My parent camera is {microscope.camera}, " + f"and my parent stage is {microscope.stage}." + ) + + return response + + def rename(self, new_name): + """ + Rename the microscope + """ + + microscope = find_component("org.openflexure.microscope") + + microscope.name = new_name + microscope.save_settings() + + +# Create your extension object +my_extension = MyExtension() diff --git a/docs/source/extensions/example_extension/02_adding_views.py b/docs/source/extensions/example_extension/02_adding_views.py new file mode 100644 index 00000000..034d8e3d --- /dev/null +++ b/docs/source/extensions/example_extension/02_adding_views.py @@ -0,0 +1,73 @@ +from labthings.extensions import BaseExtension +from labthings import find_component, fields +from labthings.views import View + +## Extension methods + + +def identify(microscope): + """ + Demonstrate access to Microscope.camera, and Microscope.stage + """ + + response = ( + f"My name is {microscope.name}. " + f"My parent camera is {microscope.camera}, " + f"and my parent stage is {microscope.stage}." + ) + + return response + + +def rename(microscope, new_name): + """ + Rename the microscope + """ + + microscope.name = new_name + microscope.save_settings() + + +## Extension views + + +class ExampleIdentifyView(View): + def get(self): + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Return our identify function's output + return identify(microscope) + + +class ExampleRenameView(View): + # Expect a request parameter called "name", which is a string. + # Passed to the argument "args". + args = fields.String(required=True, example="My Example Microscope") + + def post(self, args): + # Look for our new name in the request body + new_name = args + + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Pass microscope and new name to our rename function + rename(microscope, new_name) + + # Return our identify function's output + return identify(microscope) + + +## Create extension + +# Create your extension object +my_extension = BaseExtension("com.myname.myextension", version="0.0.0") + +# Add methods to your extension +my_extension.add_method(identify, "identify") +my_extension.add_method(rename, "rename") + +# Add API views to your extension +my_extension.add_view(ExampleIdentifyView, "/identify") +my_extension.add_view(ExampleRenameView, "/rename") diff --git a/docs/source/extensions/example_extension/03_marshaling_data.py b/docs/source/extensions/example_extension/03_marshaling_data.py new file mode 100644 index 00000000..a3048e42 --- /dev/null +++ b/docs/source/extensions/example_extension/03_marshaling_data.py @@ -0,0 +1,73 @@ +from labthings.extensions import BaseExtension +from labthings import find_component, Schema, fields +from labthings.views import View + +## Extension methods + + +# Define which properties of a Microscope object we care about, +# and what types they should be converted to +class MicroscopeIdentifySchema(Schema): + name = fields.String() # Microscopes name + id = fields.UUID() # Microscopes unique ID + state = fields.Dict() # Status dictionary + camera = fields.String() # Camera object (represented as a string) + stage = fields.String() # Stage object (represented as a string) + + +def rename(microscope, new_name): + """ + Rename the microscope + """ + + microscope.name = new_name + microscope.save_settings() + + +## Extension views + +class ExampleIdentifyView(View): + # Format our returned object using MicroscopeIdentifySchema + schema = MicroscopeIdentifySchema() + + def get(self): + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Return our microscope object, + # let schema handle formatting the output + return microscope + + +class ExampleRenameView(View): + # Format our returned object using MicroscopeIdentifySchema + schema = MicroscopeIdentifySchema() + # Expect a request parameter called "name", which is a string. Pass to argument "args". + args = {"name": fields.String(required=True, example="My Example Microscope")} + + def post(self, args): + # Look for our "name" parameter in the request arguments + new_name = args.get("name") + + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Pass microscope and new name to our rename function + rename(microscope, new_name) + + # Return our microscope object, + # let schema handle formatting the output + return microscope + + +## Create extension + +# Create your extension object +my_extension = BaseExtension("com.myname.myextension", version="0.0.0") + +# Add methods to your extension +my_extension.add_method(rename, "rename") + +# Add API views to your extension +my_extension.add_view(ExampleIdentifyView, "/identify") +my_extension.add_view(ExampleRenameView, "/rename") diff --git a/docs/source/extensions/example_extension/04_properties.py b/docs/source/extensions/example_extension/04_properties.py new file mode 100644 index 00000000..8d8af770 --- /dev/null +++ b/docs/source/extensions/example_extension/04_properties.py @@ -0,0 +1,88 @@ +from labthings.extensions import BaseExtension +from labthings import find_component, Schema, fields +from labthings.views import View, PropertyView + +## Extension methods + + +# Define which properties of a Microscope object we care about, +# and what types they should be converted to +class MicroscopeIdentifySchema(Schema): + name = fields.String() # Microscopes name + id = fields.UUID() # Microscopes unique ID + state = fields.Dict() # Status dictionary + camera = fields.String() # Camera object (represented as a string) + stage = fields.String() # Stage object (represented as a string) + + +def rename(microscope, new_name): + """ + Rename the microscope + """ + + microscope.name = new_name + microscope.save_settings() + + +## Extension views + +# Since we only have a GET method here, it'll register as a read-only property +class ExampleIdentifyView(PropertyView): + # Format our returned object using MicroscopeIdentifySchema + schema = MicroscopeIdentifySchema() + + def get(self): + """ + Show identifying information about the current microscope object + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Return our microscope object, + # let schema handle formatting the output + return microscope + + +# We can use a single schema as the input and output will be formatted identically +# Eg. We always expect a "name" string argument, and always return a "name" string attribute +class ExampleRenameView(PropertyView): + schema = {"name": fields.String(required=True, example="My Example Microscope")} + + def get(self): + """ + Show the current microscope name + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + return microscope + + def post(self, args): + """ + Change the current microscope name + """ + # Look for our "name" parameter in the request arguments + new_name = args.get("name") + + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Pass microscope and new name to our rename function + rename(microscope, new_name) + + # Return our microscope object, + # let schema handle formatting the output + return microscope + + +## Create extension + +# Create your extension object +my_extension = BaseExtension("com.myname.myextension", version="0.0.0") + +# Add methods to your extension +my_extension.add_method(rename, "rename") + +# Add API views to your extension +my_extension.add_view(ExampleIdentifyView, "/identify") +my_extension.add_view(ExampleRenameView, "/rename") diff --git a/docs/source/extensions/example_extension/05_actions.py b/docs/source/extensions/example_extension/05_actions.py new file mode 100644 index 00000000..bbeca0e7 --- /dev/null +++ b/docs/source/extensions/example_extension/05_actions.py @@ -0,0 +1,125 @@ +from labthings.extensions import BaseExtension +from labthings import find_component, Schema, fields +from labthings.views import View, ActionView, PropertyView + +from flask import send_file # Used to send images from our server +import io # Used in our capture action + +## Extension methods + + +# Define which properties of a Microscope object we care about, +# and what types they should be converted to +class MicroscopeIdentifySchema(Schema): + name = fields.String() # Microscopes name + id = fields.UUID() # Microscopes unique ID + state = fields.Dict() # Status dictionary + camera = fields.String() # Camera object (represented as a string) + stage = fields.String() # Stage object (represented as a string) + + +def rename(microscope, new_name): + """ + Rename the microscope + """ + + microscope.name = new_name + microscope.save_settings() + + +## Extension views + +# Since we only have a GET method here, it'll register as a read-only property +class ExampleIdentifyView(PropertyView): + # Format our returned object using MicroscopeIdentifySchema + schema = MicroscopeIdentifySchema() + + def get(self): + """ + Show identifying information about the current microscope object + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Return our microscope object, + # let schema handle formatting the output + return microscope + + + +# We can use a single schema as the input and output will be formatted identically +# Eg. We always expect a "name" string argument, and always return a "name" string attribute +class ExampleRenameView(PropertyView): + schema = {"name": fields.String(required=True, example="My Example Microscope")} + + def get(self): + """ + Show the current microscope name + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + return microscope + + def post(self, args): + """ + Change the current microscope name + """ + # Look for our "name" parameter in the request arguments + new_name = args.get("name") + + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Pass microscope and new name to our rename function + rename(microscope, new_name) + + # Return our microscope object, + # let schema handle formatting the output + return microscope + + +class QuickCaptureAPI(ActionView): + """ + Take an image capture and return it without saving + """ + # Expect a "use_video_port" boolean, which defaults to True if none is given + args = {"use_video_port": fields.Boolean(missing=True)} + + # Our success response (200) returns an image (image/jpeg mimetype) + responses = { + 200: {"content_type": "image/jpeg"} + } + + def post(self, args): + """ + Take a non-persistant image capture. + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Open a BytesIO stream to be destroyed once request has returned + with io.BytesIO() as stream: + + # Capture to our stream object + microscope.camera.capture(stream, use_video_port=args.get("use_video_port")) + + # Rewind the stream + stream.seek(0) + + # Return our image data using Flasks send_file function + return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg") + + +## Create extension + +# Create your extension object +my_extension = BaseExtension("com.myname.myextension", version="0.0.0") + +# Add methods to your extension +my_extension.add_method(rename, "rename") + +# Add API views to your extension +my_extension.add_view(ExampleIdentifyView, "/identify") +my_extension.add_view(ExampleRenameView, "/rename") +my_extension.add_view(QuickCaptureAPI, "/quick-capture") diff --git a/docs/source/extensions/example_extension/06_tasks_locks.py b/docs/source/extensions/example_extension/06_tasks_locks.py new file mode 100644 index 00000000..526a92a4 --- /dev/null +++ b/docs/source/extensions/example_extension/06_tasks_locks.py @@ -0,0 +1,87 @@ +from labthings.extensions import BaseExtension +from labthings import find_component, Schema, fields, update_action_progress +from labthings.views import View, ActionView, PropertyView + +from flask import send_file # Used to send images from our server +import io # Used in our capture action +import time # Used in our timelapse function + +# Used in our timelapse function +from openflexure_microscope.captures.capture_manager import generate_basename + + +## Extension methods + + +def timelapse(microscope, n_images, t_between): + """ + Save a set of images in a timelapse + + Args: + microscope: Microscope object + n_images (int): Number of images to take + t_between (int/float): Time, in seconds, between sequential captures + """ + base_file_name = generate_basename() + folder = "TIMELAPSE_{}".format(base_file_name) + + # Take exclusive control over both the camera and stage + with microscope.camera.lock, microscope.stage.lock: + for n in range(n_images): + # Generate a filename + filename = f"{base_file_name}_image{n}" + # Create a file to save the image to + output = microscope.camera.new_image( + filename=filename, folder=folder, temporary=False + ) + + # Capture + microscope.camera.capture(output) + + # Add system metadata + output.put_metadata(microscope.metadata, system=True) + + # Update task progress (only does anyting if the function is running in a LabThings task) + progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent + update_action_progress(progress_pct) + + # Wait for the specified time + time.sleep(t_between) + + +## Extension views + + +class TimelapseAPI(ActionView): + """ + Take a series of images in a timelapse + """ + args = { + "n_images": fields.Integer( + required=True, example=5, description="Number of images" + ), + "t_between": fields.Number( + missing=1, example=1, description="Time (seconds) between images" + ), + } + + def post(self, args): + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Start "timelapse" + return timelapse( + microscope, args.get("n_images"), args.get("t_between") + ) + + +## Create extension + +# Create your extension object +my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0") + +# Add methods to your extension +my_extension.add_method(timelapse, "timelapse") + +# Add API views to your extension +my_extension.add_view(TimelapseAPI, "/timelapse") diff --git a/docs/source/extensions/example_extension/07_ev_gui.py b/docs/source/extensions/example_extension/07_ev_gui.py new file mode 100644 index 00000000..d32910e8 --- /dev/null +++ b/docs/source/extensions/example_extension/07_ev_gui.py @@ -0,0 +1,125 @@ +from labthings.extensions import BaseExtension +from labthings import find_component, Schema, fields, update_action_progress +from labthings.views import View, ActionView, PropertyView + +from flask import send_file # Used to send images from our server +import io # Used in our capture action +import time # Used in our timelapse function + +# Used in our timelapse function +from openflexure_microscope.captures.capture_manager import generate_basename + +# Used to convert our GUI dictionary into a complete eV extension GUI +from openflexure_microscope.api.utilities.gui import build_gui + +## Extension methods + + +def timelapse(microscope, n_images, t_between): + """ + Save a set of images in a timelapse + + Args: + microscope: Microscope object + n_images (int): Number of images to take + t_between (int/float): Time, in seconds, between sequential captures + """ + base_file_name = generate_basename() + folder = "TIMELAPSE_{}".format(base_file_name) + + # Take exclusive control over both the camera and stage + with microscope.camera.lock, microscope.stage.lock: + for n in range(n_images): + # Generate a filename + filename = f"{base_file_name}_image{n}" + # Create a file to save the image to + output = microscope.camera.new_image( + filename=filename, folder=folder, temporary=False + ) + + # Capture + microscope.camera.capture(output) + + # Add system metadata + output.put_metadata(microscope.metadata, system=True) + + # Update task progress (only does anyting if the function is running in a LabThings task) + progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent + update_action_progress(progress_pct) + + # Wait for the specified time + time.sleep(t_between) + + +## Extension views + + +class TimelapseAPI(ActionView): + """ + Take a series of images in a timelapse, running as a background task + """ + args = { + "n_images": fields.Integer( + required=True, example=5, description="Number of images" + ), + "t_between": fields.Number( + missing=1, example=1, description="Time (seconds) between images" + ), + } + + def post(self, args): + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Create and start "timelapse", running in a background task + return timelapse( + microscope, args.get("n_images"), args.get("t_between") + ) + + +## Extension GUI (OpenFlexure eV) +# Alternate form without any dynamic parts +extension_gui = { + "icon": "timelapse", # Name of an icon from https://material.io/resources/icons/ + "forms": [ # List of forms. Each form is a collapsible accordion panel + { + "name": "Start a timelapse", # Form title + "route": "/timelapse", # The URL rule (as given by "add_view") of your submission view + "isTask": True, # This forms submission starts a background task + "isCollapsible": False, # This form cannot be collapsed into an accordion + "submitLabel": "Start", # Label for the form submit button + "schema": [ # List of dictionaries. Each element is a form component. + { + "fieldType": "numberInput", + "name": "n_images", # Name of the view arg this value corresponds to + "label": "Number of images", + "min": 1, # HTML number input attribute + "default": 5, # HTML number input attribute + }, + { + "fieldType": "numberInput", + "name": "t_between", + "label": "Time (seconds) between images", + "min": 0.1, # HTML number input attribute + "step": 0.1, # HTML number input attribute + "default": 1, # HTML number input attribute + }, + ], + } + ], +} + + +## Create extension + +# Create your extension object +my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0") + +# Add methods to your extension +my_extension.add_method(timelapse, "timelapse") + +# Add API views to your extension +my_extension.add_view(TimelapseAPI, "/timelapse") + +# Add OpenFlexure eV GUI to your extension +my_extension.add_meta("gui", build_gui(extension_gui, my_extension)) diff --git a/docs/source/extensions/introduction.rst b/docs/source/extensions/introduction.rst new file mode 100644 index 00000000..b1da263a --- /dev/null +++ b/docs/source/extensions/introduction.rst @@ -0,0 +1,21 @@ +Introduction +============ + +Extensions allow functionality to be added to the OpenFlexure Microscope web API without having to modify the base code. +They have full access to the :py:class:`openflexure_microscope.Microscope` object, +including direct access to any attached :py:class:`openflexure_microscope.camera.base.BaseCamera` and :py:class:`openflexure_stage.stage.OpenFlexureStage` objects. +This also allows access to the :py:class:`picamera.PiCamera` object. + +Extensions can either be loaded from a single Python file, or as a Python package installed to the environment being used. + +Single-file extensions +---------------------- +For adding simple functionality, such as a few basic functions and API routes, a single Python file can be loaded as a extension. This Python file must contain all of your extension objects, and be located in the applications extensions directory (by default ``/var/openflexure/extensions/microscope_extensions``). + +Package extensions +------------------ +Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions `_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope extension. + +Your module must be a folder within the extensions folder (by default ``/var/openflexure/extensions/microscope_extensions``), and include a top-level ``__init__.py`` file which includes (or imports) all of your extension objects. + +In order to enable a globally installed, packaged extension, create a file in the applications extensions directory (by default ``/var/openflexure/extensions/microscope_extensions``) which imports your extension object(s) from your module. diff --git a/docs/source/extensions/lifecycle_hooks.rst b/docs/source/extensions/lifecycle_hooks.rst new file mode 100644 index 00000000..451618a4 --- /dev/null +++ b/docs/source/extensions/lifecycle_hooks.rst @@ -0,0 +1,53 @@ +Lifecycle Hooks +=============== + +Introduction +------------ +In some cases it is useful to have functions triggered by events in an extensions lifecycle. Currently two such lifecycle events can be used, ``on_register``, and ``on_component``. + +``on_register`` +--------------- + +The ``on_register`` method can be used to have a function call as soon as the extension has been successfully registered to the microscope. For example: + +.. code-block:: python + + class MyExtension(BaseExtension): + def __init__(self): + + # Track if the extension has been registered + self.registered = False + + # Add lifecycle hooks + self.on_register(self.on_register_handler, args=(), kwargs={}) + + # Superclass init function + super().__init__("com.myname.myextension", version="0.0.0") + + def on_register_handler(self, *args, **kwargs): + self.registered = True + print("Extension has been registered!") + + +``on_component`` +---------------- + +The ``on_component`` method can be used to have a function call as soon as a particular LabThings component has been added. This can be used, for example, to get information about the microscope instance as soon as it is available. For example: + +.. code-block:: python + + class MyExtension(BaseExtension): + def __init__(self): + + # Hold a reference to the microscope object as soon as it is available + self.microscope = None + + # Add lifecycle hooks + self.on_component("com.myname.myextension", self.on_microscope_handler) + + # Superclass init function + super().__init__("org.openflexure.microscope", version="0.0.0") + + def on_microscope_handler(self, microscope_object): + print("Microscope object has been found!") + self.microscope = microscope_object \ No newline at end of file diff --git a/docs/source/extensions/marshaling.rst b/docs/source/extensions/marshaling.rst new file mode 100644 index 00000000..9ee8f384 --- /dev/null +++ b/docs/source/extensions/marshaling.rst @@ -0,0 +1,171 @@ +Marshaling data +=============== + +Introduction +------------ + +The OpenFlexure Microscope Server makes use of the `Marshmallow library `_ for both response and argument marshaling. From the Marshmallow documentation: + + **marshmallow** is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes. + + In short, marshmallow schemas can be used to: + + - **Validate** input data. + - **Deserialize** input data to app-level objects. + - **Serialize** app-level objects to primitive Python types. The serialized objects can then be rendered to standard formats such as JSON for use in an HTTP API. + +When developing extensions, you are encouraged to make use of your View ``schema`` and ``args`` class attributes to handle serialisation of your API responses, and parsing of request parameters respectively. + +Schemas and fields +++++++++++++++++++ + +A **field** describes the data type of a single parameter, as well as any other properties of that parameter for use in parsing, and documentation. For example, a String-type field, with a default value in case no actual value is passed, and extra documentation, may look like: + +.. code-block:: python + + fields.String(required=False, missing="Default value", example="Example value") + +A **schema** is a collection of keys and fields describing how an object should be serialized/deserialized. Schemas can be created in several ways, either by creating a ``Schema`` class, or by passing a dictionary of key-field pairs. Both methods will be discussed in the following examples. + + +Argument parsing +++++++++++++++++ + +In the previous section we saw how to use fields and ``args`` to get simple arguments from requests, in which a single parameter is required. By making use of Marshmallow schemas, and the `Webargs library `_, we can allow for more complex requests containing many parameters of different types. The parsed request parameters are then passed to the view function as a positional argument (as before), in the form of a dictionary. + +For example, if you are creating an API route, in which you expect parameters ``name``, ``age``, and optionally, ``job``, your schema class may look like: + +.. code-block:: python + + from labthings.server.schema import Schema + from labthings.server import fields + + class UserSchema(Schema): + name = fields.String(required=True) + age = fields.Integer(required=True) + job = fields.String(required=False, missing="Unknown") + +To inform your POST method to expect these arguments, use the ``args`` class attribute: + +.. code-block:: python + + class MyView(View): + args = UserSchema() + + def post(self, args): + .. + +Alternatively, if your schema is only used in a single location, it may be simpler to create a dictionary schema only where it is used, for example: + +.. code-block:: python + + class MyView(View): + args = { + "name": fields.String(required=True), + "age": fields.Integer(required=True), + "job": fields.String(required=False, missing="Unknown") + } + + def post(self, args): + ... + +A compatible request body, in JSON format, may look like: + +.. code-block:: json + + { + "name": "John Doe", + "age": 45, + "job": "Python developer" + } + + +This JSON data is the parsed, converted into a Python dictionary, and passed as an argument. Retreiving the data from within your view function may therefore look like: + +.. code-block:: python + + class MyView(View): + args = { + "name": fields.String(required=True), + "age": fields.Integer(required=True), + "job": fields.String(required=False, missing="Unknown") + } + + def post(self, args): + name = args.get("name") # Returns "John Doe", type str + age = args.get("age") # Returns 45, type int + job = args.get("job") # Returns "Python developer", type str + + +Object serialization +++++++++++++++++++++ + +Schemas can also be used to format our data so that it is suitable for an API response. Our API expects JSON formatted data both in, and out. It is therefore important that your API views respond with valid JSON where possible. + +Continuing with our example in the previous pages, we will enhance our ``identify`` method to provide more, better formatted information about our current microscope. + +We start by creating a schema to describe how to serialise a :py:class:`openflexure_microscope.Microscope` object. + +.. code-block:: python + + # Define which properties of a Microscope object we care about, + # and what types they should be converted to + class MicroscopeIdentifySchema(Schema): + name = fields.String() # Microscopes name + id = fields.UUID() # Microscopes unique ID + state = fields.Dict() # Status dictionary + camera = fields.String() # Camera object (represented as a string) + stage = fields.String() # Stage object (represented as a string) + + +We use this new schema in our ``identify`` view like so: + +.. code-block:: python + + class ExampleIdentifyView(View): + # Format our returned object using MicroscopeIdentifySchema + schema = MicroscopeIdentifySchema() + + def get(self): + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Return our microscope object, + # let schema handle formatting the output + return microscope + +Note that our ``get`` method now returns the :py:class:`openflexure_microscope.Microscope` object itself. No formatting is done by the function, it is entirely handled by the view class, and its `schema` attribute. Additionally, since we defined our schema as a class, it can be re-used elsewhere. + +For our ``rename`` view, we will use a simpler schema for our input arguments, defined by a dictionary (since we are only expecting a single parameter in, and it will likely not be re-used elsewhere). Our response, however, will use our ``MicroscopeIdentifySchema`` class. This means that the *response* of our ``identify`` and ``rename`` views will be identically formatted. + +Our ``rename`` view class may now look like: + +.. code-block:: python + + class ExampleRenameView(View): + # Format our returned object using MicroscopeIdentifySchema + schema = MicroscopeIdentifySchema() + # Expect a request parameter called "name", which is a string. Pass to argument "args". + args = {"name": fields.String(required=True, example="My Example Microscope")} + + def post(self, args): + # Look for our "name" parameter in the request arguments + new_name = args.get("name") + + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Pass microscope and new name to our rename function + rename(microscope, new_name) + + # Return our microscope object, + # let schema handle formatting the output + return microscope + + +Complete example +++++++++++++++++ + +Combining both of these into our example extension, we now have: + +.. literalinclude:: ./example_extension/03_marshaling_data.py \ No newline at end of file diff --git a/docs/source/extensions/properties.rst b/docs/source/extensions/properties.rst new file mode 100644 index 00000000..3a86e2d5 --- /dev/null +++ b/docs/source/extensions/properties.rst @@ -0,0 +1,114 @@ +Thing Properties +================ + +Introduction +------------ + +As well as generating Swagger documentation, the server will generate a draft `W3C Thing Description `_ . This description allows the microscope's features to be understood in a common "Web of Things" language. + +Thing Properties "expose state of the Thing. This state can then be retrieved (read) and optionally updated (write)." For the microscope, this includes the current read-only state, such as if the microscope has real camera or stage hardware attached, as well as read-write states like camera settings, and the microscope name. + +The property description for a view will be generated automatically from your available view methods, any schema decorators used, and any docstrings added to the view. + + +Defining Thing Properties +------------------------- + +In order to register a view as a Thing property, we use the ``PropertyView`` class, like so: + +.. code-block:: python + + # Since we only have a GET method here, it'll register as a read-only property + class ExampleIdentifyView(PropertyView): + # Format our returned object using MicroscopeIdentifySchema + schema = MicroscopeIdentifySchema() + + def get(self): + """ + Show identifying information about the current microscope object + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Return our microscope object, + # let schemah handle formatting the output + return microscope + + +Property schema +--------------- + +For read-write properties, it is best practice for the expected request arguments, and the views responses, to follow the same format. In this way, by looking at the response of a GET request, one can know the type of data expected in by a PUT request. + +For example, if your GET request returns the JSON: + +.. code-block:: json + + { + "name": "John Doe", + "age": 45, + "job": "Python developer" + } + +and your property supports PUT requests (for updating data), then a valid PUT request could contain the data: + +.. code-block:: json + + { + "age": 46, + "job": "Landscape gardener" + } + +This request would update the property, such that a GET request would *now* return: + +.. code-block:: json + + { + "name": "John Doe", + "age": 46, + "job": "Landscape gardener" + } + +In Property Views the ``schema`` class attribute acts as the schema for both marshalling responses *and* parsing arguments. This is because property requests and responses should be identically formatted. + +We will implement the ``schema`` attribute in our ``ExampleRenameView`` view from our previous example: + +.. code-block:: python + + # We can use a single schema as the input and output will be formatted identically + # Eg. We always expect a "name" string argument, and always return a "name" string attribute + class ExampleRenameView(PropertyView): + schema = {"name": fields.String(required=True, example="My Example Microscope")} + + def get(self): + """ + Show the current microscope name + """ + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + return microscope + + def post(self, args): + """ + Change the current microscope name + """ + # Look for our "name" parameter in the request arguments + new_name = args.get("name") + + # Find our microscope component + microscope = find_component("org.openflexure.microscope") + + # Pass microscope and new name to our rename function + rename(microscope, new_name) + + # Return our microscope object, + # let schema handle formatting the output + return microscope + +Complete example +---------------- + +Combining these into our example extension, we now have: + +.. literalinclude:: ./example_extension/04_properties.py diff --git a/docs/source/extensions/structure.rst b/docs/source/extensions/structure.rst new file mode 100644 index 00000000..053d4a4a --- /dev/null +++ b/docs/source/extensions/structure.rst @@ -0,0 +1,36 @@ +Basic extension structure +========================= + +An extension starts as a simple instance of :py:class:`labthings.server.extensions.BaseExtension`. +Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components. + +In order to access the currently running microscope object, use the :py:func:`labthings.server.find.find_component` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above. + +A simple extension file, with no API views but application-available methods may look like: + +.. literalinclude:: ./example_extension/01_basic_structure.py + + +Once this extension is loaded, any other extensions will have access to your methods: + +.. code-block:: python + + from labthings.server.find import find_extension + + def test_extension_method(): + # Find your extension. Returns None if it hasn't been found. + my_found_extension = find_extension("com.myname.myextension") + + # Call a function from your extension + if my_found_extension: + my_found_extension.identify() + + +Subclassing ``BaseExtension`` +------------------------------- + +The syntax used above allows novice programmers to easily start building extensions, without having to deal with subclassing. However, for more complex extensions which require persistent state, subclassing :py:class:`labthings.server.extensions.BaseExtension` is recommended. + +The same simple extension as seen above can be written using subclassing: + +.. literalinclude:: ./example_extension/01b_basic_structure_subclass.py diff --git a/docs/source/extensions/tasks_locks.rst b/docs/source/extensions/tasks_locks.rst new file mode 100644 index 00000000..f8ac83ae --- /dev/null +++ b/docs/source/extensions/tasks_locks.rst @@ -0,0 +1,85 @@ +Threads and Locks +================= + +Introduction +------------ + +Some actions in your extension may perform tasks that take a long time (compared to the expected response time of a web request). For example, if you were to implement a timelapse feature, this inherently runs over a long time. + +This introduces a couple of problems. Firstly, a request that triggers a long function will, by default, block the Python interpreter for the duration of the function. This usually causes the connection to timeout, and the response will never be revieved. + +Similarly, if your functionality takes a long time, it may be possible for other requests to interfere with your function. For example, in our hypothetical timelapse extension, while the timelapse is running, another user could open a connection and start moving the stage around, ruining the timelapse. + +We get around these issues by making use of action threads, and component locks. + +Action threads +-------------- + +Action threads are introduced to manage long-running functions in a way that does not block HTTP requests. Any API Action will automatically run as a background thread. + +Internally, the :class:`labthings.LabThing` object stores a list of all requested actions, and their states. This state stores the running status of the action (if itis idle, running, error, or success), information about the start and end times, a unique ID, and, upon completion, the return value of the long-running function. + +By using threads, a function can be started in the background, and it's return value fetched at a later time once it has reported success. If a long-running action is started by some client, it should note the ID returned in the action state JSON, and use this to periodically check on the status of that particular action. + +API routes have been created to allow checking the state of all actions (GET ``/actions``), a particular action by ID (GET ``/actions/``), and terminating or removing individual actions (DELETE ``/actions/``). + +All actions will return a serialized representation of the action state when your POST request returns. If the action completes within a default timeout period (usually 1 second) then the completed action representation will be returned. If the action is still running after this timeout period, the "in-progress" action representation will be returned. The final output value can then be retrieved at a later time. + +Most users will not need to create instances of this class. Instead, they will be created automatically when a function is started by an API Action view. + +An example of a long running task may look like: + +.. code-block:: python + + ... + from labthings.server.view import ActionView + + class SlowAPI(ActionView): + def post(self): + # Return the task object. + return long_running_function(function_argument_1, function_argument_2) + +After some time, once the task has completed, it could be retreived using: + +.. code-block:: python + + ... + from labthings import current_labthing + + def get_result(action_id): + matching_action = current_labthing().actions.get(task_id) + return matching_action.state + +or by making GET requests to the ``http://microscope.local/api/v2/tasks/`` view. + + +Updating action progress +++++++++++++++++++++++++ + +Some applications such as OpenFlexure eV are able to display progress bars showing the progress of an action thread. Implementing progress updates in your extension is made easy with the :py:meth:`labthings.update_action_progress` function. This function takes a single argument, which is the action progress as an integer percent (0 - 100). + +If your long running function was started within a background thread, this function will update the state of the corresponding action thread object. If your function is called outside of a long-running task (e.g. by another extension, directly), then this function will silently do nothing. + +An example of task progress is included in the example later on this page. + + +Component Locks +--------------- + +Locks have been implemented to solve a distinct issue, most obvious when considering long-running actions. During a long action such as a tile-scan or autofocus, it is absolutely necesarry to block any completing interaction with the microscope hardware. For example, even if the stage is not actively moving (for example during a capture phase within a tile scan), another user should not be able to move the microscope, interrupting the action. Thread locks act to prevent this. + +The camera and stage both contain an instance of :py:class:`labthings.lock.StrictLock`, named ``lock``. Built-in functions such as capture and move will always acquire this lock for the duration of the function. This ensures that, for example, simultaneous attemps to move do not occur. + +More importantly, however, threads can hold on to these locks for longer periods of time, blocking any other calls to the hardware. + +Locks are acquired using context managers, i.e. ``with component.lock: ...`` + + +Complete example +---------------- + +Implementing both action threads and locks in a new timelapse extension may look like: + +.. literalinclude:: ./example_extension/06_tasks_locks.py + +Notice that even though we never use the stage here, our ``timelapse`` function still acquires the stage lock. This means that during the timelapse, no other user is able to move the stage, or take separate captures. Control of the microscope is handed exclusively to the thread that obtains the lock, which in this case is the thread spawned when handling the POST request. diff --git a/docs/source/extensions/views.rst b/docs/source/extensions/views.rst new file mode 100644 index 00000000..be8364fb --- /dev/null +++ b/docs/source/extensions/views.rst @@ -0,0 +1,34 @@ +Adding web API views +==================== + +Introduction +------------ +Extensions can create views to expose extension functionality via the web API. Creating API views for your extension is strongly recommended, as this is the primary way we encourage interaction with the microscope device. + +As with most HTTP APIs, we make use of basic HTTP request methods. GET requests return data without modifying any state. POST requests completely replace data with data passed as request arguments. PUT requests update data with new data passed as request arguments. DELETE requests delete a particular object from the server. Your API views need not implement all of these methods. + +Continuing our example on the previous page, and discussed below, adding API views may look like: + +.. literalinclude:: ./example_extension/02_adding_views.py + +Note that we are now passing our microscope object as an argument to our API methods. Finding the microscope component is performed by the API view at request-time, and passed onto the functions. + +In this case, our extension will have two new API views at `/identify` and `/rename`. The `/identify` view only accepts GET requests, and the `/rename` view only accepts POST requests. + +Request arguments ++++++++++++++++++ + +For POST and PUT requests, data usually needs to be provided to the view in order to perform its function. In this example, our ``rename`` view requires a new microscope name to be passed. We make use of the ``args`` class attribute to provide this functionality. + +``args`` defines the type of data expected in the request body. In this example, we use ``String`` type data. The arguments of ``fields.String`` allow us to provide additional information, such as the parameter being required, and example values to appear in API documentation. + +Adding additional fields, and the meaning of the field types, will be discussed further in the next section. + +When a POST request is made to our API view, the server converts the body of the request into a ``String``, and passes it as a positional argument to our ``post`` function. + +Swagger documentation ++++++++++++++++++++++ + +At this point, it is useful to introduce the automatically generated Swagger documentation. From any web browser, go to ``http://microscope.local/api/v2/docs/swagger-ui`` (or replace ``microscope.local`` with your microscope's IP address if ``microscope.local`` doesn't work for your system). + +This page uses `SwaggerUI `_ to provide visual, interactive API documentation. Find your extensions URL in the documentation under the ``extensions`` group. Basic documentation about the parameters required for your POST method should be visible, as well as an interactive example filled out with the example request given in the view ``schema``. diff --git a/docs/source/index.rst b/docs/source/index.rst index 28ef7180..d4d13333 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,6 +9,7 @@ Welcome to OpenFlexure Microscope Software's documentation! config.rst microscope.rst camera.rst + stage.rst plugins.rst api.rst diff --git a/docs/source/microscope.rst b/docs/source/microscope.rst index b706eb2c..55a96cdb 100644 --- a/docs/source/microscope.rst +++ b/docs/source/microscope.rst @@ -1,9 +1,11 @@ Microscope class ======================================================= +The main microscope class handles microscope settings, passing these between the settings file and their appropriate components (camera, stage), basic metadata about the current device status, and interfacing with the separate camera and stage components. + .. toctree:: :maxdepth: 2 :caption: Contents: -.. automodule:: openflexure_microscope - :members: \ No newline at end of file +.. automodule:: openflexure_microscope.microscope + :members: \ No newline at end of file diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index b7134ad0..bb865f26 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -1,13 +1,15 @@ -Developing Plugins -======================================================= +Developing API Extensions +========================= .. toctree:: :maxdepth: 2 - ./plugins/introduction.rst - ./plugins/structure.rst - ./plugins/routes.rst - ./plugins/hardware.rst - ./plugins/schema.rst - ./plugins/example.rst - ./plugins/class.rst \ No newline at end of file + ./extensions/introduction.rst + ./extensions/structure.rst + ./extensions/views.rst + ./extensions/marshaling.rst + ./extensions/properties.rst + ./extensions/actions.rst + ./extensions/tasks_locks.rst + ./extensions/ev_gui.rst + ./extensions/lifecycle_hooks.rst \ No newline at end of file diff --git a/docs/source/plugins/class.rst b/docs/source/plugins/class.rst deleted file mode 100644 index 205a7071..00000000 --- a/docs/source/plugins/class.rst +++ /dev/null @@ -1,38 +0,0 @@ -Classes and Modules -=================== - -Plugin class ------------- -.. autoclass:: openflexure_microscope.plugins.MicroscopePlugin - :members: - -.. autoclass:: openflexure_microscope.api.v1.views.MicroscopeViewPlugin - :members: - -Task module ------------ -.. automodule:: openflexure_microscope.task - :members: - -Lock module ------------ -.. automodule:: openflexure_microscope.lock - :members: - -Default plugins ---------------- - -Autofocus -+++++++++ -.. automodule:: openflexure_microscope.plugins.default.autofocus - :members: - -Camera calibration -++++++++++++++++++ -.. automodule:: openflexure_microscope.plugins.default.camera_calibration - :members: - -Scan and Stack -++++++++++++++ -.. automodule:: openflexure_microscope.plugins.default.scan - :members: \ No newline at end of file diff --git a/docs/source/plugins/example.rst b/docs/source/plugins/example.rst deleted file mode 100644 index 5d0ba18b..00000000 --- a/docs/source/plugins/example.rst +++ /dev/null @@ -1,21 +0,0 @@ -Example plugin --------------- - -plugin.py -+++++++++ - -.. literalinclude:: example/plugin.py - :language: python - -schema.json -+++++++++++ - -.. literalinclude:: example/schema.json - :language: JSON - -Notes -+++++ - -In this example, if the package or file were named ``my_plugin``, the three microscope plugin methods would be accessible from ``.plugin.my_plugin.identify()``, ``.plugin.my_plugin.timelapse()``, and ``.plugin.my_plugin.hello_world()``. - -Web API routes would automatically be set up at ``/api/v1/plugin/my_plugin/identify`` (GET), ``/api/v1/plugin/my_plugin/timelapse`` (POST), and ``/api/v1/plugin/my_plugin/hello`` (GET, POST). \ No newline at end of file diff --git a/docs/source/plugins/example/plugin.py b/docs/source/plugins/example/plugin.py deleted file mode 100644 index 4a3baea5..00000000 --- a/docs/source/plugins/example/plugin.py +++ /dev/null @@ -1,146 +0,0 @@ -from openflexure_microscope.plugins import MicroscopePlugin -from openflexure_microscope.api.v1.views import MicroscopeViewPlugin -from openflexure_microscope.api.utilities import JsonPayload - -import os -import time -import json - -from flask import request, Response, escape, jsonify - -HERE = os.path.dirname(os.path.realpath(__file__)) -SCHEMA_PATH = os.path.join(HERE, "schema.json") - -### MICROSCOPE PLUGIN ### - -class MyPluginClass(MicroscopePlugin): - """ - A set of default plugins - """ - - global SCHEMA_PATH - - with open(SCHEMA_PATH, 'r') as sc: - api_schema = json.load(sc) - - api_views = { - '/identify': IdentifyAPI, - '/hello': HelloWorldAPI, - '/timelapse': TimelapseAPI, - } - - def identify(self): - """ - Demonstrate access to Microscope.camera, and Microscope.stage - """ - - response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, - self.microscope.stage) - return response - - def hello_world(self): - """ - Demonstrate passive method - """ - - return "Hello world!" - - def timelapse(self, n_images): - """ - Demonstrate a long-running method that requires microscope hardware - """ - print("Starting timelapse...") - capture_array = [] # Empty list to store captures in - - # Acquire locks. Exception is raised if lock is in use by another thread. - with self.microscope.camera.lock, self.microscope.stage.lock: - for _ in range(n_images): - - # Create a data stream to capture to - capture_data = self.microscope.camera.new_image( - write_to_file=True, - temporary=False) - - # Capture a still image from the Pi camera, into the data stream - self.microscope.camera.capture( - capture_data, - use_video_port=True) - - # Append the capture data to our list - capture_array.append(capture_data) - - # Wait for 1 minute - time.sleep(60) - - -### API VIEWS ### - -class IdentifyAPI(MicroscopeViewPlugin): - """ - A simple example API plugin, attached through the main microscope plugin. - """ - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut - data = self.plugin.identify() - return Response(escape(data)) - - -class HelloWorldAPI(MicroscopeViewPlugin): - """ - A method to create, set, and return a new microscope parameter. - """ - - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - - # If the microscope does not already contain our plugin_string attribute - if not hasattr(self.microscope, 'plugin_string'): - # Make a string, using the MicroscopeViewPlugin.plugin shortcut - self.microscope.plugin_string = self.plugin.hello_world() - - json_response = jsonify({ - 'plugin_string': self.microscope.plugin_string - }) - - return Response(json_response) - - def post(self): - """ - Method to call when an HTTP POST request is made. - Assumes request will include a JSON payload. - """ - # Get payload JSON - payload = JsonPayload(request) - - # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty. - new_plugin_string = payload.param('plugin_string', default='', convert=str) - - if new_plugin_string: # If not None or empty - # Set microscope attribute to the specified string - self.microscope.plugin_string = new_plugin_string - - json_response = jsonify({ - 'plugin_string': self.microscope.plugin_string - }) - - return Response(json_response) - -class TimelapseAPI(MicroscopeViewPlugin): - def post(self): - - # Get any JSON data in the body of the POST request - payload = JsonPayload(request) - - # Extract the "n_images" parameter if it was passed. Otherwise, default to 10. - n_images = payload.param('n_images', default=10, convert=int) - - # Attach the long-running method as a microscope task - self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images) - - # Return the state of the task (will show ID, start time, and status before the task has finished) - return jsonify(self.timelapse_task.state), 202 \ No newline at end of file diff --git a/docs/source/plugins/example/schema.json b/docs/source/plugins/example/schema.json deleted file mode 100644 index 58364150..00000000 --- a/docs/source/plugins/example/schema.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "id": "example-plugin", - "icon": "pets", - "forms": [ - { - "name": "Hello world", - "isTask": false, - "selfUpdate": true, - "route": "/hello", - "schema": [ - { - "fieldType": "textInput", - "placeholder": "Enter a string", - "label": "Plugin string", - "name": "plugin_string" - } - ] - }, - { - "name": "Timelapse", - "isTask": true, - "route": "/timelapse", - "submitLabel": "Start timelapse", - "schema": [ - { - "fieldType": "numberInput", - "name": "n_images", - "label": "Number of images" - } - ] - } - ] - } \ No newline at end of file diff --git a/docs/source/plugins/hardware.rst b/docs/source/plugins/hardware.rst deleted file mode 100644 index 8c622064..00000000 --- a/docs/source/plugins/hardware.rst +++ /dev/null @@ -1,145 +0,0 @@ -Accessing the microscope -======================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - -Introduction ------------- -All instances of :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin` must be attached via a microscope plugin. As a result of this, the ``MicroscopeViewPlugin`` class has a ``self.microscope`` attribute, allowing direct access to the :py:class:`openflexure_microscope.Microscope` object. This means that web API plugins can simply chain together basic microscope functions and expose new API routes. No additional microscope functionality is required. However, in most cases web API plugins will serve to provide API routes to new microscope functionality defined in a microscope plugin. - - -Tasks and Locks ---------------- -Two principles have been implemented to regulate and synchronise access to the microscope hardware: tasks, and locks. -Especially when writing plugins with API routes, careful use of both tasks and locks should be implemented. - -Tasks -+++++ -Tasks are introduced to manage long-running functions in a way that does not block HTTP requests. Without -the use of tasks, long-running functions would block an HTTP response until the function returns, often -resulting in a timeout. - -To prevent this, any function can be offloaded to a background task. This is done through the microscope's ``task`` object, -by calling ``.task.start(, *args, **kwargs)``. Internally, the ``task`` object stores a list -of all requested tasks, which themselves contain a ``state`` dictionary. This dictionary stores the status of the task (if it -is idle, running, error, or success), information about the start and end times, a unique task ID, and the return value of -the long-running function. - -By using tasks, a function can be started in the background, and it's return value fetched at a later time once it has reported -success. If a long-running task is started by some client, it should note the ID returned in the task state JSON, and use this to -periodically check on the status of that particular task. API routes have been created to allow checking the state of all tasks -(GET `/task`), a particular task by ID (GET `/task/`), removing individual tasks (DELETE `/task/`), and pruning -the task list of any no-longer-running tasks (DELETE `/task`). - -An example of a long running task may look like: - -.. code-block:: python - - from openflexure_microscope.plugins import MicroscopeViewPlugin - from openflexure_microscope.exceptions import TaskDeniedException - - class MyPlugin(MicroscopeViewPlugin): - def post(self): - # Attach the long-running method as a microscope task - try: - self.my_task = self.microscope.task.start(self.plugin.long_running_function) - return jsonify(self.my_task.state), 202 - - except TaskDeniedException: - return abort(409) - -After some time, once the task has completed, it could be retreived using: - -.. code-block:: python - - ... - def get_result(self): - self.my_task.state['status'] == "success": - return self.my_task.state['return'] - -Locks -+++++ -Locks have been implemented to solve a distinct issue, most obvious when considering long-running tasks. During -a long task such as a tile-scan or autofocus, it is absolutely necesarry to block any completing interaction with -the microscope hardware. For example, even if the stage is not actively moving (for example during a capture phase -within a tile scan), another user should not be able to move the microscope, interrupting the task. Thread locks act -to prevent this. - -The camera and stage both contain an instance of :py:class:`openflexure_microscope.lock.StrictLock`, named ``lock``. -Built-in functions such as capture and move will always acquire this lock for the duration of the function. This ensures -that, for example, simultaneous attemps to move do not occur. - -More importantly, however, tasks can hold on to these locks for longer periods of time, blocking any other calls to the hardware. - -For example, a timelapse plugin may look like: - -.. code-block:: python - - from openflexure_microscope.plugins import MicroscopePlugin - from openflexure_microscope.exceptions import TaskDeniedException - from openflexure_microscope.api.v1.views import MicroscopeViewPlugin - from openflexure_microscope.api.utilities import JsonPayload - - - ### MICROSCOPE PLUGIN ### - - class MyPluginClass(MicroscopePlugin): - - api_views = { - '/timelapse': TimelapseAPI, - } - - def timelapse(self, n_images): - """ - Demonstrate a long-running method that requires microscope hardware - """ - print("Starting timelapse...") - capture_array = [] # Empty list to store captures in - - # Acquire locks. Exception is raised if lock is in use by another thread. - with self.microscope.camera.lock, self.microscope.stage.lock: - for _ in range(n_images): - - # Create a data stream to capture to - capture_data = self.microscope.camera.new_image( - write_to_file=True, - temporary=False) - - # Capture a still image from the Pi camera, into the data stream - self.microscope.camera.capture( - capture_data, - use_video_port=True) - - # Append the capture data to our list - capture_array.append(capture_data) - - # Wait for 1 minute - time.sleep(60) - - return capture_array - - - ### API ROUTES ### - - class TimelapseAPI(MicroscopeViewPlugin): - def post(self): - - # Get any JSON data in the body of the POST request - payload = JsonPayload(request) - - # Extract the "n_images" parameter if it was passed. Otherwise, default to 10. - n_images = payload.param('n_images', default=10, convert=int) - - # Attach the long-running method as a microscope task - self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images) - - # Return the state of the task (will show ID, start time, and status before the task has finished) - return jsonify(self.timelapse_task.state), 202 - - -Notice that even though we never use the stage here, we still acquire the lock. This means that during the timelapse, -no other user is able to move the stage, or take separate captures. Control of the microscope is handed exclusively -to the thread that obtains the lock, which in this case is the thread spawned when handling a POST request: -``self.microscope.task.start(self.plugin.timelapse, n_images)``. diff --git a/docs/source/plugins/introduction.rst b/docs/source/plugins/introduction.rst deleted file mode 100644 index 50d163fb..00000000 --- a/docs/source/plugins/introduction.rst +++ /dev/null @@ -1,41 +0,0 @@ -Introduction -============ - -Plugins allow functionality to be added to the OpenFlexure Microscope without having to modify the base code. -They have full access to the :py:class:`openflexure_microscope.Microscope` object, -including direct access to the attached :py:class:`openflexure_microscope.camera.pi.StreamingCamera` and :py:class:`openflexure_stage.stage.OpenFlexureStage` objects. -This also allows direct access to the :py:class:`picamera.PiCamera` object. - -All microscope plugins must subclass :py:class:`openflexure_microscope.plugins.MicroscopePlugin` in order to be loaded by the microscopes :py:class:`openflexure_microscope.plugins.PluginMount`. - -Once attached, all methods defined within the plugin class will be accessible from ``.plugin..``. -Here, ```` is an instance of :py:class:`openflexure_microscope.Microscope`. The ```` is the method defined within your plugin class. Finally, ```` is determined differently depending on the type of plugin. - -Plugins can either be loaded as a single Python file located anywhere on disk, or as a Python package installed to the environment being used. If loaded from a single file, the namespace is set to the file name (excluding .py extension) of the plugin file. If loaded from a package, the namespace is set to the name of the top-level module in the package. For example, if your plugin class definition resides within ``my_openflexure_plugins.microscope.mypluginpackage``, the namespace will be set to ``mypluginpackage``. Where possible, try to use descriptive, unique package names for this reason. For example, rather than name your plugin package ``autofocus``, which would like cause namespace clashes, instead name it ``yourname_autofocus``, or similar. - -Module (single-file) plugins ----------------------------- -For adding simple functionality, such as a few basic functions and API routes, a single Python file can be loaded as a plugin. - -This Python file must contain all of your plugin classes. Relative imports will not work. External modules and packages can be used with absolute imports, however for more complex plugins, it is often worth instead making use of an installable package plugin. - -Package plugins ---------------- -Generally, for adding anything other than very simple functionality, plugins should be written as `package distributions `_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope plugin. - -The main restriction is that the plugin package must be importable using an absolute import from within the Python environment being used to load your microscope. - -Loading plugins with microscope_settings.yaml ---------------------------------------------- -Both types of plugin are loaded by specifying the plugin class in your :ref:`MicroscopeRC`. In the case of a single-file plugin, specify the path to the plugin file, followed by the name of your :py:class:`openflexure_microscope.plugins.MicroscopePlugin` child class, separated by a colon. For packaged plugins, specify the absolute module name in place of the path. - -For example, the plugins section of your microscope_settings.yaml file may look like: - -.. code-block:: yaml - - ... - plugins: - - openflexure_microscope.plugins.default:Plugin - - ~/my_plugins/my_plugin_file.py:MyPluginClass - - my_openflexure_plugins.microscope.mypluginpackage:MyPluginClass - ... diff --git a/docs/source/plugins/routes.rst b/docs/source/plugins/routes.rst deleted file mode 100644 index a7c2618b..00000000 --- a/docs/source/plugins/routes.rst +++ /dev/null @@ -1,151 +0,0 @@ -Adding web API routes -===================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - -Introduction ------------- -Plugins can automatically create routes to expose plugin functionality via the web API. Creating API routes for your plugin is strongly recommended, as this is the primary way we encourage interaction with the microscope device. - -To create API routes, add a dictionary to your plugin class, named ``api_views``. Within this dictionary, each key should be a string defining the route URL, whose value is a class, subclassing :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin`. - -For example, your ``api_views`` dictionary may look like: - -.. code-block:: python - - class MyPluginClass(MicroscopePlugin): - - api_views = { - '/myplugin': MyRouteAPI, - } - -Here, ``MyRouteAPI`` is a web API plugin class, subclassing :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin`. If your plugin package were named ``myplugins.package``, an API route would be automatically added at ``/api/v1/plugin/myplugins/package/myplugin``. - -The full URL that your plugin will attach to is essentially identical to it's full module path. That is, if your plugin is loaded from ``my_microscope_plugins.mypluginpackage:MyPluginClass``, then your plugin routes will appear at ``/plugin/my_microscope_plugins/mypluginpackage/``. While this means that plugin routes can get long very quickly, they will generally only ever be accessed by client applications, and so this generally should not be a problem. - -The MicroscopeViewPlugin class ------------------------------- - -All API plugin classes must subclass :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin`, which is itself a subclass of `Flask's MethodView `_. This greatly simplifies defining different functionality associated with different HTTP methods at a single URL route. -It is best practice to clearly separate out types of functionality by HTTP method. For example, a GET request should never change the state of the microscope. For this, POST or PUT requests are acceptable. Parameters should be passed to POST and PUT requests as JSON payloads, and your methods should include fallback code for cases where parameters are not passed, or are passed in an invalid format. The DELETE method should only be used in situations where your plugin creates additional URL routes for newly created objects, and should serve only to delete these objects and routes. - -Each HTTP method maps to a function with the same name, in lowercase. For example, your :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin` may look like: - -.. code-block:: python - - from flask import jsonify - ... - class MyRouteAPI(MicroscopeViewPlugin): - - def get(self): - # Retrieve some information, without changing the state of the microscope - ... - - def post(self): - # Change the state of the microscope based on passed parameters - ... - -Sometimes you will need to create variable API routes. For example, the built-in routes for managing capture data use capture IDs in the request URL to specify which capture data should be returned. Plugins can also access this functionality. This is done using `Flask variable rules `_. Here, variables are added to the URL route string by marking them with ````, which then passes ``variable_name`` to your request function as a keyword argument. - -For example: - -.. code-block:: python - - class MyPluginClass(MicroscopePlugin): - - api_views = { - '/myplugin/': MyRouteAPI, - } - ... - - class MyRouteAPI(MicroscopeViewPlugin): - - def get(self, object_id): - # Retrieve some information about object_id - this_object = object_dictionary[object_id] - ... - -Calling plugin methods from routes -++++++++++++++++++++++++++++++++++ - -Instances of MicroscopeViewPlugin have direct access to their associated microscope plugin methods, without needing to know the plugin namespace in advance. As described earlier in this section, all plugins get attached to the microscope in their own namespace, based on the plugins name. This means there are two equivalent ways to access your plugin methods from a web API plugin: - -.. code-block:: python - - ... - # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut - self.plugin.my_plugin_method() - - # Call a method from our plugin, using the full route - self.microscope.my_plugin_name.my_plugin_method() - ... - -Building responses -++++++++++++++++++ - -Since we are using Flask as our web framework, you're able to return any type of `Flask response `_ to be passed back to the client. However, since the server is designed to be accessed by clients as a simple API, it is **strongly** recommended that responses are JSON formatted. Both GET and POST methods should return a JSON object describing the current state of the microscope relevant to the action performed. - -Fortunately, Flask includes a ``jsonify`` method to convert any Python dictionary into a valid JSON response. See the `Flask documentation `_ for more information. - -It is also possible to create HTTP errors using the `Flask abort method `_. This is useful particularly if your route handles multiple resources such as captures. In the case that a client tries to modify or get the state of a nonexistent resource, you can return (for example) ``abort(404)``, or some other `more relevant HTTP error code `_. - -An example web route with simple responses may look like: - -.. code-block:: python - - from flask import jsonify - ... - - class MyPluginClass(MicroscopePlugin): - - api_views = { - '/myplugin/': MyRouteAPI, - } - ... - - class MyRouteAPI(MicroscopeViewPlugin): - - def get(self, object_id): - - # If the requested object doesn't exist - if not object_id in object_dictionary: - # Abort with error 404. This prevents raising a nonspecific exception later - abort(404) - - # Retrieve some information, now we're sure the reqested object exists - this_object = object_dictionary[object_id] - - # Make a dictionary of the data to return - data = { - 'id': this_object.id, - 'name': this_object.name - 'object_value', this_object.value - } - - # Return the JSON representation of our data dictionary - return jsonify(data) - ... - -Parsing JSON from HTTP POST requests ------------------------------------- -To ease obtaining values from a JSON payload attached to an HTTP POST request, you can use the :py:class:`openflexure_microscope.api.utilities.JsonPayload` class. This allows parameters to be extracted by their key, with a default value supplied to avoid errors associated with nonexistent keys. Additionally, a converter function may be passed, used in the following examples to convert the type of a value. In principle, however, you can pass any single-argument function and it will apply that function to the value, if it exists. - -.. code-block:: python - - ... - class MyRouteAPI(MicroscopeViewPlugin): - - def post(self): - # Get payload JSON from request - payload = JsonPayload(request) - - # Try to find value associated with 'my_string' key. - # If that key doesn't exist in the payload, return '' instead. - # If a value does exist, convert it to a string, regardless of its original type. - new_plugin_string = payload.param('my_string', default='', convert=str) - ... - -.. autoclass:: openflexure_microscope.api.utilities.JsonPayload - :members: diff --git a/docs/source/plugins/schema/json_schema.rst b/docs/source/plugins/schema/json_schema.rst deleted file mode 100644 index c7cf118e..00000000 --- a/docs/source/plugins/schema/json_schema.rst +++ /dev/null @@ -1,5 +0,0 @@ -Full JSON Schema -================ - -.. literalinclude:: schema.json - :language: JSON diff --git a/docs/source/plugins/schema/schema.json b/docs/source/plugins/schema/schema.json deleted file mode 100644 index 063e63cd..00000000 --- a/docs/source/plugins/schema/schema.json +++ /dev/null @@ -1,187 +0,0 @@ -{ - "definitions": {}, - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://example.com/root.json", - "type": "object", - "title": "The Root Schema", - "required": [ - "id", - "icon", - "forms" - ], - "properties": { - "id": { - "$id": "#/properties/id", - "type": "string", - "title": "Unique ID", - "description": "A unique name for the plugin UI", - "default": "", - "examples": [ - "test-plugin" - ], - "pattern": "^(.*)$" - }, - "icon": { - "$id": "#/properties/icon", - "type": "string", - "title": "Icon name", - "description": "Name of the Material Design icon to use for the plugin tab", - "default": "", - "examples": [ - "pets" - ], - "pattern": "^(.*)$" - }, - "forms": { - "$id": "#/properties/forms", - "type": "array", - "title": "Forms array", - "description": "Array of form descriptions to be displayed", - "default": null, - "items": { - "$id": "#/properties/forms/items", - "type": "object", - "title": "The Items Schema", - "required": [ - "name", - "route", - "schema" - ], - "properties": { - "name": { - "$id": "#/properties/forms/items/properties/name", - "type": "string", - "title": "Form name", - "description": "Human-readable form name", - "default": "", - "examples": [ - "Simple request" - ], - "pattern": "^(.*)$" - }, - "isCollapsible": { - "$id": "#/properties/forms/items/properties/isCollapsible", - "type": "boolean", - "title": "The isCollapsible Schema", - "description": "Should the form be rendered as a collapsible item", - "default": false, - "examples": [ - true - ] - }, - "isTask": { - "$id": "#/properties/forms/items/properties/isTask", - "type": "boolean", - "title": "The isTask Schema", - "description": "Should the form's submit function be treated as a long-running task", - "default": false, - "examples": [ - false - ] - }, - "selfUpdate": { - "$id": "#/properties/forms/items/properties/selfUpdate", - "type": "boolean", - "title": "The selfUpdate Schema", - "description": "Should the form's component values be automatically updated with a GET request", - "default": false, - "examples": [ - true - ] - }, - "route": { - "$id": "#/properties/forms/items/properties/route", - "type": "string", - "title": "The Route Schema", - "description": "Form submit POST request's corresponding api_views route", - "default": "", - "examples": [ - "/do" - ], - "pattern": "^(.*)$" - }, - "submitLabel": { - "$id": "#/properties/forms/items/properties/submitLabel", - "type": "string", - "title": "The submitLabel Schema", - "description": "String to place in submit button", - "default": "Submit", - "examples": [ - "Do my function" - ], - "pattern": "^(.*)$" - }, - "schema": { - "$id": "#/properties/forms/items/properties/schema", - "type": "array", - "title": "Form component array", - "items": { - "$id": "#/properties/forms/items/properties/schema/items", - "type": "object", - "title": "A form component", - "required": [ - "fieldType", - "name" - ], - "properties": { - "fieldType": { - "$id": "#/properties/forms/items/properties/schema/items/properties/fieldType", - "type": "string", - "title": "Component type", - "default": "", - "examples": [ - "numberInput", - "textInput", - "radioList", - "checkList", - "htmlBlock", - "selectList", - "textInput", - "keyvalList", - "taglList" - ], - "pattern": "^(.*)$" - }, - "placeholder": { - "$id": "#/properties/forms/items/properties/schema/items/properties/placeholder", - "type": "string", - "title": "Component placeholder value", - "default": "", - "examples": [ - "Enter your name" - ], - "pattern": "^(.*)$" - }, - "name": { - "$id": "#/properties/forms/items/properties/schema/items/properties/name", - "type": "string", - "title": "Unique component name", - "examples": [ - "my_form_component_1" - ], - "pattern": "^(.*)$" - }, - "label": { - "$id": "#/properties/forms/items/properties/schema/items/properties/label", - "type": "string", - "title": "Component label (if applicable)", - "default": "", - "examples": [ - "My form component" - ], - "pattern": "^(.*)$" - }, - "value": { - "$id": "#/properties/forms/items/properties/schema/items/properties/value", - "type": ["array", "boolean", "integer", "number", "object", "string"], - "title": "Component value", - "pattern": "^(.*)$" - } - } - } - } - } - } - } - } - } \ No newline at end of file diff --git a/docs/source/plugins/schema_example.json b/docs/source/plugins/schema_example.json deleted file mode 100644 index 07b348d2..00000000 --- a/docs/source/plugins/schema_example.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "id": "test-plugin", - "icon": "pets", - "forms": [ - { - "name": "Simple request", - "isCollapsible": false, - "isTask": false, - "selfUpdate": true, - "route": "/do", - "submitLabel": "Do things", - "schema": [ - { - "fieldType": "numberInput", - "placeholder": "Some integer", - "name": "val_int", - "label": "Number value" - }, - { - "fieldType": "textInput", - "placeholder": "Some string", - "label": "String value", - "name": "val_str" - }, - { - "fieldType": "radioList", - "name": "val_radio", - "label": "Radio value", - "options": ["First", "Second", "Third"] - }, - { - "fieldType": "checkList", - "name": "val_check", - "label": "Checklist values", - "options": ["Foo", "Bar", "Baz"] - }, - { - "fieldType": "htmlBlock", - "name": "html_block", - "content": "This is a block of HTML in a plugin!" - }, - { - "fieldType": "selectList", - "name": "val_select", - "multi": false, - "label": "Some selection", - "options": ["Most", "Average", "Least"] - }, - { - "fieldType": "textInput", - "placeholder": "Some string", - "label": "Non-persistent string", - "name": "val_disposable" - }, - { - "fieldType": "keyvalList", - "label": "A key-val object", - "name": "val_keyval", - "value": { - "key1": "foo", - "key2": "bar" - } - }, - { - "fieldType": "tagList", - "label": "A tag list", - "name": "val_tags", - "value": ["tag2", "tag2", "squidward"] - } - ] - }, - { - "name": "Task form", - "isTask": true, - "selfUpdate": true, - "route": "/task", - "submitLabel": "Start task", - "schema": [ - { - "fieldType": "numberInput", - "name": "run_time", - "label": "Run time (seconds)" - } - ] - } - ] - } \ No newline at end of file diff --git a/docs/source/plugins/structure.rst b/docs/source/plugins/structure.rst deleted file mode 100644 index 44cb3152..00000000 --- a/docs/source/plugins/structure.rst +++ /dev/null @@ -1,33 +0,0 @@ -Basic plugin structure -=========================== - -As described earlier, a plugin must subclass :py:class:`openflexure_microscope.plugins.MicroscopePlugin`. Each plugin in described by a single class, containing any number of methods. The methods defined within your plugin class will be attached to the microscope. By subclassing :py:class:`openflexure_microscope.plugins.MicroscopePlugin`, your methods automatically get access to the microscope object your plugin is attached to, through ``self.microscope``. This is an instance of :py:class:`openflexure_microscope.Microscope`, and gives unrestricted access to the attached camera and stage hardware. - -For example, a simple plugin file named ``myplugin.py``, may look like: - -.. code-block:: python - - from openflexure_microscope.plugins import MicroscopePlugin - - class MyPlugin(MicroscopePlugin): - - def identify(self): - """ - Demonstrate access to Microscope.camera, and Microscope.stage - """ - - parent_camera = self.microscope.camera - parent_stage = self.microscope.stage - - response = "My parent camera is {}, and my parent stage is {}.".format(parent_camera, - parent_stage) - return response - - def hello_world(self): - """ - Demonstrate passive method - """ - - return "Hello world!" - -When this plugin is loaded and attached to a microscope object named ``microscope``, the plugin methods will be available at ``microscope.plugin.myplugin.identify()`` and ``microscope.plugin.myplugin.hello_world()``. \ No newline at end of file diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 10e66e7a..abeaaf45 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -5,22 +5,15 @@ Install ------- Stable installation -+++++++++++++++ -For most users, this is the reccommended installation method. ++++++++++++++++++++ +For most users, the OpenFlexure Microscope software should be installed using our `pre-built Raspbian SD card image. `_ -- Run ``curl -LSs get.openflexure.org/microscope |sudo bash`` - - See the `GitLab repo `_ for details. -- Follow on-screen prompts - -Developer and non-interactive installation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The installer script can pull the latest development package from our git repository, and use install into a developer environment using Poetry. -Options also exist to run the installer without any user prompts. - -See the `installer script GitLab repo `_ for details. Manual installation +++++++++++++++++++ + +For offline (i.e. no real microscope connected) development, a basic development server can run on any system. + - (Recommended) create and activate a virtual environment - Install non-python dependencies with ``sudo apt-get install libatlas-base-dev libjasper-dev libjpeg-dev`` - Install `Poetry `_, clone this repo, and ``poetry install`` from inside the repo. @@ -28,6 +21,6 @@ Manual installation Managing the server ------------------- -Managing the server through the installer script's CLI is documented `on our website `_. +Managing the server through the installer script's CLI is documented `on our website `_. This includes starting the server as a background service, as well as starting a development server with real-time debug logging. \ No newline at end of file diff --git a/docs/source/sangastage.rst b/docs/source/sangastage.rst new file mode 100644 index 00000000..941def8a --- /dev/null +++ b/docs/source/sangastage.rst @@ -0,0 +1,5 @@ +Sangaboard Microscope Stage +=========================== + +.. automodule:: openflexure_microscope.stage.sanga + :members: \ No newline at end of file diff --git a/docs/source/stage.rst b/docs/source/stage.rst new file mode 100644 index 00000000..dcf61e2f --- /dev/null +++ b/docs/source/stage.rst @@ -0,0 +1,8 @@ +Stage Class +=========== + +.. toctree:: + :maxdepth: 2 + + sangastage.rst + basestage.rst diff --git a/openflexure_microscope/__init__.py b/openflexure_microscope/__init__.py index 8cd70322..e69de29b 100644 --- a/openflexure_microscope/__init__.py +++ b/openflexure_microscope/__init__.py @@ -1,8 +0,0 @@ -__all__ = ['Microscope', 'config', 'task', 'lock', 'utilities'] -__version__ = "0.1.0" - -from .microscope import Microscope -from . import config -from . import utilities -from . import task -from . import lock diff --git a/openflexure_microscope/api/__init__.py b/openflexure_microscope/api/__init__.py index daa8c0a6..e69de29b 100644 --- a/openflexure_microscope/api/__init__.py +++ b/openflexure_microscope/api/__init__.py @@ -1,3 +0,0 @@ -__all__ = ['utilities'] - -from . import utilities diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 6f964dc5..99670586 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -1,207 +1,190 @@ #!/usr/bin/env python - +import sys import time import atexit -import logging -import sys +import logging, logging.handlers + +# Look for debug flag +if "-d" in sys.argv or "--debug" in sys.argv: + log_level = logging.DEBUG +else: + log_level = logging.INFO + + +# Set root logger level +root_log = logging.getLogger() +root_log.setLevel(log_level) + + import os +import pkg_resources -from flask import ( - Flask, jsonify, send_file) +from flask import Flask, send_file, abort -from serial import SerialException from datetime import datetime -from flask_cors import CORS +from flask_cors import CORS, cross_origin -from openflexure_microscope.api.exceptions import JSONExceptionHandler -from openflexure_microscope.api.utilities import list_routes +from openflexure_microscope.api.utilities import list_routes, init_default_extensions -from openflexure_microscope import Microscope +from openflexure_microscope.config import JSONEncoder +from openflexure_microscope.paths import ( + OPENFLEXURE_VAR_PATH, + OPENFLEXURE_EXTENSIONS_PATH, + settings_file_path, + logs_file_path, +) -from openflexure_microscope.camera.capture import build_captures_from_exif -from openflexure_microscope.config import USER_CONFIG_DIR -from openflexure_microscope.api.v1 import blueprints +from labthings import create_app +from labthings.extensions import find_extensions -# Import device modules -# NB this will eventually be handled by the RC file, so you can choose what device -# class should be attached. -try: - from openflexure_microscope.camera.pi import PiCameraStreamer -except ImportError: - logging.warning("Unable to import PiCameraStreamer") -from openflexure_microscope.camera.mock import MockStreamer - -from openflexure_microscope.stage.sanga import SangaStage, SangaDeltaStage -from openflexure_microscope.stage.mock import MockStage +from openflexure_microscope.api.microscope import default_microscope as api_microscope +from openflexure_microscope.api.v2 import views # Handle logging -is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") +access_log = logging.getLogger('werkzeug') +# Block the access logs from propagating up to the root logger +access_log.propagate = False -DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, 'openflexure_microscope.log') +ROOT_LOGFILE = logs_file_path("openflexure_microscope.log") +ACCESS_LOGFILE = logs_file_path("openflexure_microscope.access.log") -if (__name__ == "__main__") or (not is_gunicorn): - # If imported, but not by gunicorn - print("Letting sys handle logs") - logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) -else: - # Direct standard Python logging to file and console - root = logging.getLogger() - error_formatter = logging.Formatter("[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s") - - rotating_logfile = logging.handlers.RotatingFileHandler( - DEFAULT_LOGFILE, - maxBytes=1000000, - backupCount=7 - ) - - error_handlers = [ - rotating_logfile, - logging.StreamHandler() - ] - - for handler in error_handlers: - handler.setFormatter(error_formatter) - root.addHandler(handler) - - root.setLevel(logging.getLogger("gunicorn.error").level) - -# Create a dummy microscope object, with no hardware attachments -api_microscope = Microscope() - -# Rebuild the capture list -# TODO: Offload to a thread? -stored_image_list = build_captures_from_exif() +# Basic log format +formatter = logging.Formatter( + "[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s" +) -# Generate API URI based on version from filename -def uri(suffix, api_version, base=None): - if not base: - base = "/api/{}".format(api_version) - return_uri = base + suffix - logging.debug("Created app route: {}".format(return_uri)) - return return_uri +# Create file handler +fh = logging.handlers.RotatingFileHandler( + ROOT_LOGFILE, maxBytes=1_000_000, backupCount=5 +) +fh.setFormatter(formatter) +fh.setLevel(logging.DEBUG) +fh.propagate = False +# Create access log file handler +afh = logging.handlers.RotatingFileHandler( + ACCESS_LOGFILE, maxBytes=1_000_000, backupCount=5 +) +afh.setFormatter(formatter) +afh.setLevel(logging.DEBUG) +afh.propagate = False +# Add file handler to root logger +root_log.addHandler(fh) +access_log.addHandler(afh) + +# Log server paths being used +logging.info(f"Running with data path {OPENFLEXURE_VAR_PATH}") + +logging.info("Creating app") # Create flask app -app = Flask(__name__) -app.url_map.strict_slashes = False +app, labthing = create_app( + __name__, + prefix="/api/v2", + title=f"OpenFlexure Microscope {api_microscope.name}", + description="Test LabThing-based API for OpenFlexure Microscope", + types=["org.openflexure.microscope"], + version=pkg_resources.get_distribution("openflexure-microscope-server").version, + flask_kwargs={"static_url_path": "", "static_folder": "static/dist"}, +) -CORS(app, resources=r'/api/*') +# Enable CORS for some routes outside of LabThings +cors = CORS(app) -# Make errors more API friendly -handler = JSONExceptionHandler(app) +# Use custom JSON encoder +labthing.json_encoder = JSONEncoder +app.json_encoder = JSONEncoder + +# Attach lab devices +labthing.add_component(api_microscope, "org.openflexure.microscope") + +# Attach extensions +if not os.path.isfile(OPENFLEXURE_EXTENSIONS_PATH): + init_default_extensions(OPENFLEXURE_EXTENSIONS_PATH) +for extension in find_extensions(OPENFLEXURE_EXTENSIONS_PATH): + labthing.register_extension(extension) + +# Attach captures resources +labthing.add_view(views.CaptureList, f"/captures") +labthing.add_root_link(views.CaptureList, "captures") + +labthing.add_view(views.CaptureView, f"/captures/") +labthing.add_view(views.CaptureDownload, f"/captures//download/") +labthing.add_view(views.CaptureTags, f"/captures//tags") +labthing.add_view(views.CaptureAnnotations, f"/captures//annotations") + +# Attach settings and state resources +labthing.add_view(views.SettingsProperty, f"/instrument/settings") +labthing.add_root_link(views.SettingsProperty, "instrumentSettings") +labthing.add_view(views.NestedSettingsProperty, "/instrument/settings/") +labthing.add_view(views.StateProperty, "/instrument/state") +labthing.add_view(views.NestedStateProperty, "/instrument/state/") +labthing.add_root_link(views.StateProperty, "instrumentState") +labthing.add_view(views.ConfigurationProperty, "/instrument/configuration") +labthing.add_view( + views.NestedConfigurationProperty, "/instrument/configuration/" +) +labthing.add_root_link(views.ConfigurationProperty, "instrumentConfiguration") -# After app starts, but before first request, attach hardware to global microscope -@app.before_first_request -def attach_microscope(): - # Create the microscope object globally (common to all spawned server threads) - global api_microscope, stored_image_list - logging.debug("First request made. Populating microscope with hardware...") +# Attach streams resources +labthing.add_view(views.MjpegStream, f"/streams/mjpeg") +labthing.add_view(views.SnapshotStream, f"/streams/snapshot") - # Initialise camera - logging.debug("Creating camera object...") - try: - api_camera = PiCameraStreamer() - except Exception as e: - logging.error(e) - logging.warning("No valid camera hardware found. Falling back to mock camera!") - api_camera = MockStreamer() - - # Initialise stage - logging.debug("Creating stage object...") - try: - api_stage = SangaDeltaStage() - except Exception as e: - logging.error(e) - logging.warning("No valid stage hardware found. Falling back to mock stage!") - api_stage = MockStage() - - # Attach devices to microscope - logging.debug("Attaching devices to microscope...") - api_microscope.attach( - api_camera, - api_stage - ) - - # Restore loaded capture array to camera object - logging.debug("Restoring captures...") - if stored_image_list: - api_microscope.camera.images = stored_image_list - - logging.debug("Microscope successfully attached!") +# Attach microscope action resources +labthing.add_view(views.actions.ActionsView, "/actions") +labthing.add_root_link(views.actions.ActionsView, "actions") +for name, action in views.enabled_root_actions().items(): + view_class = action["view_class"] + rule = action["rule"] + labthing.add_view(view_class, f"/actions{rule}") -# WEBAPP ROUTES - -# API ROUTES - -# Base routes -base_blueprint = blueprints.base.construct_blueprint(api_microscope) -app.register_blueprint(base_blueprint, url_prefix=uri('', 'v1')) - -# Stage routes -stage_blueprint = blueprints.stage.construct_blueprint(api_microscope) -app.register_blueprint(stage_blueprint, url_prefix=uri('/stage', 'v1')) - -# Camera routes -camera_blueprint = blueprints.camera.construct_blueprint(api_microscope) -app.register_blueprint(camera_blueprint, url_prefix=uri('/camera', 'v1')) - -# Plugin routes -plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope) -app.register_blueprint(plugin_blueprint, url_prefix=uri('/plugin', 'v1')) - -# Task routes -task_blueprint = blueprints.task.construct_blueprint(api_microscope) -app.register_blueprint(task_blueprint, url_prefix=uri('/task', 'v1')) +@app.route("/") +def openflexure_ev(): + return app.send_static_file("index.html") -@app.route('/routes') +@app.route("/routes") +@cross_origin() def routes(): """ List of all connected API routes - - .. :quickref: System; Routes - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: stream active """ - return jsonify(list_routes(app)) + return list_routes(app) -@app.route('/log') +@app.route("/log") def err_log(): """ Most recent 1mb of log output - - .. :quickref: System; Log - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: stream active """ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") return send_file( - DEFAULT_LOGFILE, - as_attachment=True, - attachment_filename='openflexure_microscope_{}.log'.format(timestamp) + ROOT_LOGFILE, + as_attachment=True, + attachment_filename="openflexure_microscope_{}.log".format(timestamp), ) +@app.route("/api/v1/", defaults={"path": ""}) +@app.route("/api/v1/") +def api_v1_catch_all(path): + abort(410, "API v1 is no longer in use. Please upgrade your client.") + + # Automatically clean up microscope at exit def cleanup(): - global api_microscope logging.debug("App teardown started...") logging.debug("Settling...") time.sleep(0.5) # Save config logging.debug("Saving config for teardown...") - api_microscope.save_config(backup=True) + api_microscope.save_settings() logging.debug("Settling...") time.sleep(0.5) @@ -218,5 +201,9 @@ def cleanup(): atexit.register(cleanup) +# Start the app if __name__ == "__main__": - app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False) + from labthings import Server + logging.info("Starting OpenFlexure Microscope Server...") + server = Server(app) + server.run(host="::", port=5000, debug=False, zeroconf=True) diff --git a/openflexure_microscope/api/default_extensions/__init__.py b/openflexure_microscope/api/default_extensions/__init__.py new file mode 100644 index 00000000..8a28495f --- /dev/null +++ b/openflexure_microscope/api/default_extensions/__init__.py @@ -0,0 +1,28 @@ +import logging +import traceback +from contextlib import contextmanager + + +@contextmanager +def handle_extension_error(extension_name): + """'gracefully' log an error if an extension fails to load.""" + try: + yield + except Exception as e: + logging.error( + f"Exception loading builtin extension {extension_name}: \n{traceback.format_exc()}" + ) + + +with handle_extension_error("autofocus"): + from .autofocus import autofocus_extension_v2 +with handle_extension_error("scan"): + from .scan import scan_extension_v2 +with handle_extension_error("zip builder"): + from .zip_builder import zip_extension_v2 +with handle_extension_error("autostorage"): + from .autostorage import autostorage_extension_v2 +with handle_extension_error("camera stage mapping"): + from camera_stage_mapping.ofm_extension import csm_extension +with handle_extension_error("lens shading calibration"): + from .picamera_autocalibrate import lst_extension_v2 diff --git a/openflexure_microscope/api/default_extensions/autofocus.py b/openflexure_microscope/api/default_extensions/autofocus.py new file mode 100644 index 00000000..e2c334e8 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/autofocus.py @@ -0,0 +1,409 @@ +from labthings import fields, find_component, current_action +from labthings.extensions import BaseExtension +from labthings.views import View, ActionView, PropertyView + +from openflexure_microscope.devel import JsonResponse, request, abort +from openflexure_microscope.utilities import set_properties + +import time +import numpy as np + +import logging +from scipy import ndimage +from contextlib import contextmanager + +from threading import Thread, Event + +### Autofocus utilities + + +class JPEGSharpnessMonitor: + def __init__(self, microscope, timeout=60): + self.microscope = microscope + self.camera = microscope.camera + self.stage = microscope.stage + self.jpeg_sizes = [] + self.jpeg_times = [] + self.stage_positions = [] + self.stage_times = [] + self.stop_event = Event() + self.timeout = timeout + self.keep_alive() + self.background_thread = None + + def is_alive(self): + if self.background_thread is None: + return False + else: + return self.background_thread.is_alive() + + def should_stop(self): + import time + + return time.time() - self.kept_alive > self.timeout + + def keep_alive(self): + import time + + self.kept_alive = time.time() + + def start(self): + "Start monitoring sharpness by looking at JPEG size" + if not self.camera.stream_active: + logging.warn( + "Autofocus sharpness monitor was started but the camera isn't streaming. Attempting to start the stream..." + ) + self.camera.start_stream_recording() + self.background_thread = Thread(target=self._measure_jpegs) + self.background_thread.start() + return self + + def stop(self): + "Stop the background thread" + self.stop_event.set() + logging.info("Joining JPEG thread") + self.background_thread.join() + + def _measure_jpegs(self): + "Function that runs in a background thread to record sharpness" + logging.debug("Starting sharpness measurement in background thread") + self.keep_alive() + logging.debug(f"_measure_jpegs stop_event: {self.stop_event.is_set()}") + while not self.stop_event.is_set() and not self.should_stop(): + size_now = self.jpeg_size() + time_now = time.time() + self.jpeg_sizes.append(size_now) + self.jpeg_times.append(time_now) + logging.info("Exited JPEG measure loop") + if self.stop_event.is_set(): + logging.debug("Cleanly stopped sharpness measurement in background thread") + if self.should_stop(): + logging.debug("Sharpness measurement timed out and has stopped") + + def jpeg_size(self): + """Return the size of a frame from the MJPEG stream""" + return len(self.camera.get_frame()) + + def focus_rel(self, dz, backlash=False, **kwargs): + self.keep_alive() + self.stage_times.append(time.time()) + self.stage_positions.append(self.stage.position) + self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs) + self.stage_times.append(time.time()) + self.stage_positions.append(self.stage.position) + i = len(self.stage_positions) - 2 + return i, self.stage_positions[-1][2] + + def move_data(self, istart, istop=None): + "Extract sharpness as a function of (interpolated) z" + if istop is None: + istop = istart + 2 + jpeg_times = np.array(self.jpeg_times) + jpeg_sizes = np.array(self.jpeg_sizes) + stage_times = np.array(self.stage_times)[istart:istop] + stage_zs = np.array(self.stage_positions)[istart:istop, 2] + try: + start = np.argmax(jpeg_times > stage_times[0]) + stop = np.argmax(jpeg_times > stage_times[1]) + except ValueError as e: + if np.sum(jpeg_times > stage_times[0]) == 0: + raise ValueError( + "No images were captured during the move of the stage. Perhaps the camera is not streaming images?" + ) + else: + raise e + if stop < 1: + stop = len(jpeg_times) + logging.debug("changing stop to {}".format(stop)) + jpeg_times = jpeg_times[start:stop] + jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs) + return jpeg_times, jpeg_zs, jpeg_sizes[start:stop] + + def sharpest_z_on_move(self, index): + """Return the z position of the sharpest image on a given move""" + jt, jz, js = self.move_data(index) + if len(js) == 0: + raise ValueError( + "No images were captured during the move of the stage. Perhaps the camera is not streaming images?" + ) + return jz[np.argmax(js)] + + def data_dict(self): + """Return the gathered data as a single convenient dictionary""" + data = {} + for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]: + data[k] = getattr(self, k) + return data + + +def decimate_to(shape, image): + """Decimate an image to reduce its size if it's too big.""" + decimation = np.max( + np.ceil(np.array(image.shape, dtype=np.float)[: len(shape)] / np.array(shape)) + ) + return image[:: int(decimation), :: int(decimation), ...] + + +def sharpness_sum_lap2(rgb_image): + """Return an image sharpness metric: sum(laplacian(image)**")""" + # image_bw=np.mean(decimate_to((1000,1000), rgb_image),2) + image_bw = np.mean(rgb_image, 2) + image_lap = ndimage.filters.laplace(image_bw) + return np.mean(image_lap.astype(np.float) ** 4) + + +def sharpness_edge(image): + """Return a sharpness metric optimised for vertical lines""" + gray = np.mean(image.astype(float), 2) + n = 20 + edge = np.array([[-1] * n + [1] * n]) + return np.sum( + [np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]] + ) + + +### Autofocus extension + + +def measure_sharpness(microscope, metric_fn=sharpness_sum_lap2): + """Measure the sharpness of the camera's current view.""" + if hasattr(microscope.camera, "array"): + return metric_fn(microscope.camera.array(use_video_port=True)) + + +def autofocus(microscope, dz, settle=0.5, metric_fn=sharpness_sum_lap2): + """Perform a simple autofocus routine. + The stage is moved to z positions (relative to current position) in dz, + and at each position an image is captured and the sharpness function + evaulated. We then move back to the position where the sharpness was + highest. No interpolation is performed. + dz is assumed to be in ascending order (starting at -ve values) + """ + camera = microscope.camera + stage = microscope.stage + + with set_properties(stage, backlash=256), stage.lock, camera.lock: + sharpnesses = [] + positions = [] + camera.annotate_text = "" + + for _ in stage.scan_z(dz, return_to_start=False): + if current_action() and current_action().stopped: + return + positions.append(stage.position[2]) + time.sleep(settle) + sharpnesses.append(measure_sharpness(microscope, metric_fn)) + + newposition = positions[np.argmax(sharpnesses)] + stage.move_rel([0, 0, newposition - stage.position[2]]) + + return positions, sharpnesses + + +@contextmanager +def monitor_sharpness(microscope): + m = JPEGSharpnessMonitor(microscope) + m.start() + try: + yield m + finally: + m.stop() + + +def move_and_find_focus(microscope, dz): + """Make a relative Z move and return the peak sharpness position""" + with monitor_sharpness(microscope) as m: + m.focus_rel(dz) + return m.sharpest_z_on_move(0) + + +def fast_autofocus(microscope, dz=2000, backlash=None): + """Perform a down-up-down-up autofocus""" + with monitor_sharpness( + microscope + ) as m, microscope.camera.lock, microscope.stage.lock: + i, z = m.focus_rel(-dz / 2) + i, z = m.focus_rel(dz) + fz = m.sharpest_z_on_move(i) + if backlash is None: + i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent + else: + i, z = m.focus_rel(fz - z - backlash) + m.focus_rel(fz - z) + return m.data_dict() + + +def fast_up_down_up_autofocus( + microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=25 +): + """Autofocus by measuring on the way down, and moving back up with feedback. + + This autofocus method is very efficient, as it only passes the peak once. + The sequence of moves it performs is: + + 1. Move to the top of the range `dz/2` (can be disabled) + + 2. Move down by `dz` while monitoring JPEG size to find the focus. + + 3. Move back up to the `target_z` position, relative to the sharpest image. + + 4. Measure the sharpness, and compare against the curve recorded in (2) to \\ + estimate how much further we need to go. Make this move, to reach our \\ + target position. + + Moving back to the target position in two steps allows us to correct for + backlash, by using the sharpness-vs-z curve as a rough encoder for Z. + + Parameters: + dz: number of steps over which to scan (optional, default 2000) + + target_z: we aim to finish at this position, relative to focus. This may + be useful if, for example, you want to acquire a stack of images in Z. + It is optional, and the default value of 0 will finish at the focus. + + initial_move_up: (optional, default True) set this to `False` to move down + from the starting position. Mostly useful if you're able to combine + the initial move with something else, e.g. moving to the next scan point. + + mini_backlash: (optional, default 25) is a small extra move made in step + 3 to help counteract backlash. It should be small enough that you + would always expect there to be greater backlash than this. Too small + might slightly hurt accuracy, but is unlikely to be a big issue. Too big + may cause you to overshoot, which is a problem. + """ + with monitor_sharpness( + microscope + ) as m, microscope.camera.lock, microscope.stage.lock: + # Ensure the MJPEG stream has started + microscope.camera.start_stream_recording() + + df = dz # TODO: refactor so I actually use dz in the code below! + logging.debug("Initial move") + if initial_move_up: + m.focus_rel(df / 2) + # move down + logging.debug("Move down") + i, z = m.focus_rel(-df) + # now inspect where the sharpest point is, and estimate the sharpness + # (JPEG size) that we should find at the start of the Z stack + logging.debug("Get target_s") + jt, jz, js = m.move_data(i) + best_z = jz[np.argmax(js)] + target_s = np.interp( + [best_z + target_z], jz[::-1], js[::-1] + ) # NB jz is decreasing + + # now move to the start of the z stack + logging.debug("Move to the start of the z stack") + i, z = m.focus_rel( + best_z + target_z - z + mini_backlash + ) # takes us to the start of the stack + + # We've deliberately undershot - figure out how much further we should move based on the curve + logging.debug("Calculate remining movement") + current_js = m.jpeg_size() + imax = np.argmax(js) # we want to crop out just the bit below the peak + js = js[imax:] # NB z is in DECREASING order + jz = jz[imax:] + inow = np.argmax( + js < current_js + ) # use the curve we recorded to estimate our position + # TODO: fancy interpolation stuff + + # So, the Z position corresponding to our current sharpness value is zs[inow] + # That means we should move forwards, by best_z - zs[inow] + logging.debug("Correction move") + correction_move = best_z + target_z - jz[inow] + logging.debug( + "Fast autofocus scan: correcting backlash by moving {} steps".format( + correction_move + ) + ) + m.focus_rel(correction_move) + return m.data_dict() + + +class MeasureSharpnessAPI(View): + def post(self): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to measure sharpness.") + + return {"sharpness": measure_sharpness(microscope)} + + +class AutofocusAPI(ActionView): + """ + Run a standard autofocus + """ + args = {"dz": fields.List(fields.Int())} + + def post(self, args): + payload = JsonResponse(request) + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + # Figure out the range of z values to use + dz = np.array(args.get("dz", np.linspace(-300, 300, 7))) + + if microscope.has_real_stage(): + logging.debug("Running autofocus...") + + # return a handle on the autofocus task + return autofocus(microscope, dz) + + else: + abort(503, "No stage connected. Unable to autofocus.") + + +class FastAutofocusAPI(ActionView): + """ + Run a fast autofocus + """ + args = { + "dz": fields.Int(missing=2000), + "backlash": fields.Int(missing=25, minimum=0) + } + + def post(self, args): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + # Figure out the parameters to use + dz = args.get("dz") + backlash = args.get("backlash") + + if microscope.has_real_stage(): + logging.debug("Running autofocus...") + + # Acquire microscope lock with 1s timeout + with microscope.lock(timeout=1): + # Run fast_up_down_up_autofocus + return fast_up_down_up_autofocus( + microscope, dz=dz, mini_backlash=backlash + ) + + else: + abort(503, "No stage connected. Unable to autofocus.") + + +autofocus_extension_v2 = BaseExtension( + "org.openflexure.autofocus", + version="2.0.0", + description="Actions to move the microscope in Z and pick the point with the sharpest image.", +) + +autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus") +autofocus_extension_v2.add_method( + fast_up_down_up_autofocus, "fast_up_down_up_autofocus" +) +autofocus_extension_v2.add_method(autofocus, "autofocus") + +autofocus_extension_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness", endpoint="measure_sharpness") +autofocus_extension_v2.add_view(AutofocusAPI, "/autofocus", endpoint="autofocus") +autofocus_extension_v2.add_view(FastAutofocusAPI, "/fast_autofocus", endpoint="fast_autofocus") diff --git a/openflexure_microscope/api/default_extensions/autostorage.py b/openflexure_microscope/api/default_extensions/autostorage.py new file mode 100644 index 00000000..86b80821 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/autostorage.py @@ -0,0 +1,272 @@ +from labthings.extensions import BaseExtension +from labthings.views import View, PropertyView +from labthings import fields, find_component + +from openflexure_microscope.paths import settings_file_path, check_rw +from openflexure_microscope.config import OpenflexureSettingsFile +from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH +from openflexure_microscope.captures.capture import build_captures_from_exif + +from openflexure_microscope.api.utilities.gui import build_gui + +from flask import abort +import logging +import os +import psutil + +from sys import platform + + +AS_SETTINGS_PATH = settings_file_path("autostorage_settings.json") + + +def get_partitions(): + return [disk.mountpoint for disk in psutil.disk_partitions() if "rw" in disk.opts] + + +def get_permissive_partitions(): + return [partition for partition in get_partitions() if check_rw(partition)] + + +def get_permissive_locations(): + return [ + (partition, os.path.join(partition, "openflexure", "data", "micrographs")) + for partition in get_permissive_partitions() + ] + + +def get_current_location(capture_manager): + return capture_manager.paths.get("default") + + +def set_current_location(capture_manager, location: str): + if not os.path.isdir(location): + os.makedirs(location) + logging.debug("Updating location...") + capture_manager.paths.update({"default": location}) + logging.debug("Rebuilding captures...") + capture_manager.rebuild_captures() + logging.debug("Capture location changed successfully.") + + +def get_default_location(): + return BASE_CAPTURE_PATH + + +def get_all_locations(): + locations = {} + # If default is not already listed (e.g. if it's currently set) + if get_default_location() not in locations.values(): + locations["Default"] = get_default_location() + + for ppartition, plocation in get_permissive_locations(): + pdrive = os.path.splitdrive(plocation)[0] + if not ( + pdrive # If path actually has a drive (basically just Windows?) + and any( # And shares a common drive with an existing location + [ + pdrive == os.path.splitdrive(location)[0] + for location in locations.values() + ] + ) + ): + locations[ppartition] = plocation + + # Strip out Nones + return {k: v for k, v in locations.items() if v} + + +class CaptureStorageLocation: + def __init__(self, mountpoint): + pass + + +class AutostorageExtension(BaseExtension): + def __init__(self): + BaseExtension.__init__( + self, + "org.openflexure.autostorage", + version="2.0.0", + description="Handle switching capture storage devices", + ) + + # We'll store a reference to a CaptureManager object, who's capture paths will be modified + self.capture_manager = None + + self.initial_location = get_default_location() + + # Register the on_microscope function to run when the microscope is attached + self.on_component("org.openflexure.microscope", self.on_microscope) + + def on_microscope(self, microscope_obj): + """Function to automatically call when the parent LabThing has a microscope attached.""" + logging.debug(f"Autostorage extension found microscope {microscope_obj}") + if hasattr(microscope_obj, "captures"): + logging.debug( + f"Autostorage extension bound to CaptureManager {self.capture_manager}" + ) + + # Store a reference to the CaptureManager + self.capture_manager = microscope_obj.captures + # Store the initial storage location + self.initial_location = get_current_location(self.capture_manager) + + # If preferred path does not exist, or cannot be written to + self.check_location(self.initial_location) + + logging.debug(self.get_locations()) + + def check_location(self, location=None): + if not location: + location = get_current_location(self.capture_manager) + # If preferred path does not exist, or cannot be written to + if not (os.path.isdir(location) and check_rw(location)): + logging.error( + f"Preferred capture path {location} is missing or cannot be written to. Restoring defaults." + ) + # Reset the storage location to default + set_current_location(self.capture_manager, get_default_location()) + + def get_locations(self): + if self.capture_manager: + locations = get_all_locations() + + current_location = get_current_location(self.capture_manager) + if current_location not in locations.values(): + locations.update({"Custom": current_location}) + # Add location from the CaptureManager settings file + return locations + else: + return {} + + def get_preferred_key(self): + current = get_current_location(self.capture_manager) + locations = self.get_locations() + + matches = [k for k, v in locations.items() if v == current] + + if len(matches) > 1: + logging.warning( + "Multiple path matches found. Weird, but carrying on using zeroth." + ) + + return matches[0] + + def set_preferred_key(self, new_path_key: str): + if not new_path_key in self.get_locations().keys(): + raise KeyError(f"No location named {new_path_key}") + + location = self.get_locations().get(new_path_key) + set_current_location(self.capture_manager, location) + + def key_to_title(self, path_key: str): + if not path_key in self.get_locations().keys(): + raise KeyError(f"No location named {path_key}") + + return f"{path_key} ({self.get_locations().get(path_key)})" + + def title_to_key(self, path_title: str): + matches = [] + for loc_key in self.get_locations().keys(): + if path_title.startswith(loc_key): + matches.append(loc_key) + + if len(matches) > 1: + logging.warning( + "Multiple path matches found. Weird, but carrying on using zeroth." + ) + + return matches[0] + + def get_titles(self): + return [self.key_to_title(key) for key in self.get_locations().keys()] + + def get_preferred_title(self): + return self.key_to_title(self.get_preferred_key()) + + +autostorage_extension_v2 = AutostorageExtension() + + +class GetLocationsView(PropertyView): + def get(self): + global autostorage_extension_v2 + + autostorage_extension_v2.check_location() + return autostorage_extension_v2.get_locations() + + +class PreferredLocationView(PropertyView): + schema = fields.String(required=True, example="Default") + + def get(self): + global autostorage_extension_v2 + + autostorage_extension_v2.check_location() + return autostorage_extension_v2.get_preferred_key() + + def post(self, new_path_key): + global autostorage_extension_v2 + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + autostorage_extension_v2.check_location() + autostorage_extension_v2.set_preferred_key(new_path_key) + microscope.save_settings() + + +class PreferredLocationGUIView(View): + args = {"new_path_title": fields.String(required=True)} + + def post(self, args): + global autostorage_extension_v2 + + new_path_title = args.get("new_path_title") + logging.debug(new_path_title) + + new_path_key = autostorage_extension_v2.title_to_key(new_path_title) + logging.debug(f"{new_path_key}") + + autostorage_extension_v2.check_location() + autostorage_extension_v2.set_preferred_key(new_path_key) + + return new_path_title + + +def dynamic_form(): + global autostorage_extension_v2 + autostorage_extension_v2.check_location() + return { + "icon": "sd_storage", + "title": "Storage", + "viewPanel": "gallery", + "forms": [ + { + "name": "Autostorage", + "isCollapsible": False, + "isTask": False, + "route": "/location-from-title", + "emitOnResponse": "globalUpdateCaptures", + "submitLabel": "Set path", + "schema": [ + { + "fieldType": "selectList", + "name": "new_path_title", + "label": "Capture storage path", + "options": autostorage_extension_v2.get_titles(), + "value": autostorage_extension_v2.get_preferred_title(), + } + ], + } + ], + } + + +autostorage_extension_v2.add_view(GetLocationsView, "/list-locations") +autostorage_extension_v2.add_view(PreferredLocationView, "/location") +autostorage_extension_v2.add_view(PreferredLocationGUIView, "/location-from-title") +autostorage_extension_v2.add_meta( + "gui", build_gui(dynamic_form, autostorage_extension_v2) +) diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py new file mode 100644 index 00000000..49d03097 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/__init__.py @@ -0,0 +1 @@ +from .extension import lst_extension_v2 diff --git a/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py new file mode 100644 index 00000000..af00f7f0 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/extension.py @@ -0,0 +1,129 @@ +from labthings import find_component +from labthings.views import View, ActionView +from labthings.extensions import BaseExtension + +from flask import abort + +from contextlib import contextmanager +import logging + +# Type hinting +from typing import Tuple + +from .recalibrate_utils import ( + recalibrate_camera, + auto_expose_and_freeze_settings, + flat_lens_shading_table, +) + + +@contextmanager +def pause_stream(scamera, resolution: Tuple[int, int] = None): + """This context manager locks a streaming camera, and pauses the stream. + + The stream is re-enabled, with the original resolution, once the with + block has finished. + """ + with scamera.lock: + assert ( + not scamera.record_active + ), "We can't pause the camera's video stream while a recording is in progress." + streaming = scamera.stream_active + old_resolution = scamera.camera.resolution + if streaming: + logging.info("Stopping stream in pause_stream context manager") + scamera.stop_stream_recording(resolution=resolution) + try: + yield scamera + finally: + scamera.camera.resolution = old_resolution + if streaming: + logging.info("Restarting stream in pause_stream context manager") + scamera.start_stream_recording() + + +def recalibrate(microscope): + """Reset the camera's settings. + + This generates new gains, exposure time, and lens shading + table such that the background is as uniform as possible + with a gray level of 230. It takes a little while to run. + """ + with pause_stream(microscope.camera) as scamera: + auto_expose_and_freeze_settings( + scamera.camera + ) # scamera.camera is the PiCamera object + recalibrate_camera(scamera.camera) + microscope.save_settings() + + +class RecalibrateView(ActionView): + def post(self): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to recalibrate.") + + logging.info("Starting microscope recalibration...") + + return recalibrate(microscope) + + +class FlattenLSTView(ActionView): + def post(self): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort( + 503, + "No microscope connected. Unable to flatten the lens shading table.", + ) + + try: + with pause_stream(microscope.camera) as scamera: + flat_lst = flat_lens_shading_table(scamera.camera) + scamera.camera.lens_shading_table = flat_lst + microscope.save_settings() + except: + logging.exception("Error flattening the lens shading table.") + abort( + 503, + "Couldn't flatten the lens shading table - do you have the forked PiCamera library installed?", + ) + + +class DeleteLSTView(ActionView): + def post(self): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort( + 503, + "No microscope connected. Unable to flatten the lens shading table.", + ) + + try: + with pause_stream(microscope.camera) as scamera: + scamera.camera.lens_shading_table = None + microscope.save_settings() + except: + logging.exception("Error deleting the lens shading table.") + abort( + 503, + "Couldn't flatten the lens shading table - do you have the forked PiCamera library installed?", + ) + + +lst_extension_v2 = BaseExtension( + "org.openflexure.calibration.picamera", + version="2.0.0-beta.1", + description="Routines to perform flat-field correction on the camera.", +) + +lst_extension_v2.add_method( + recalibrate, "org.openflexure.calibration.picamera.recalibrate" +) + +lst_extension_v2.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate") +lst_extension_v2.add_view(FlattenLSTView, "/flatten_lens_shading_table", endpoint="flatten_lens_shading_table") +lst_extension_v2.add_view(DeleteLSTView, "/delete_lens_shading_table", endpoint="delete_lens_shading_table") diff --git a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py similarity index 72% rename from openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py rename to openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py index 4940a77a..822264e3 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/recalibrate_utils.py +++ b/openflexure_microscope/api/default_extensions/picamera_autocalibrate/recalibrate_utils.py @@ -1,13 +1,15 @@ import numpy as np import time +import logging from picamera import PiCamera from picamera.array import PiRGBArray, PiBayerArray + def rgb_image(camera, resize=None, **kwargs): """Capture an image and return an RGB numpy array""" with PiRGBArray(camera, size=resize) as output: - camera.capture(output, format='rgb', resize=resize, **kwargs) + camera.capture(output, format="rgb", resize=resize, **kwargs) return output.array @@ -19,7 +21,9 @@ def flat_lens_shading_table(camera): library (with lens shading table support) it will raise an error. """ if not hasattr(PiCamera, "lens_shading_table"): - raise ImportError("This program requires the forked picamera library with lens shading support") + raise ImportError( + "This program requires the forked picamera library with lens shading support" + ) return np.zeros(camera._lens_shading_table_shape(), dtype=np.uint8) + 32 @@ -28,42 +32,55 @@ def adjust_exposure_to_setpoint(camera, setpoint): print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="") for i in range(3): print(".", end="") - camera.shutter_speed = int(camera.shutter_speed * setpoint / np.max(rgb_image(camera))) + camera.shutter_speed = int( + camera.shutter_speed * setpoint / np.max(rgb_image(camera)) + ) time.sleep(1) print("done") def auto_expose_and_freeze_settings(camera): """Freeze the settings after auto-exposing to white illumination""" - print("Allowing the camera to auto-expose") + logging.info("Allowing the camera to auto-expose") camera.awb_mode = "auto" camera.exposure_mode = "auto" - camera.iso = 0 # This is important, if it's on a fixed ISO, gain might not set properly. + camera.iso = ( + 0 + ) # This is important, if it's on a fixed ISO, gain might not set properly. for i in range(6): print(".", end="") time.sleep(0.5) - print("done") + logging.info("done") - print("Freezing the camera settings...") + logging.info("Freezing the camera settings...") camera.shutter_speed = camera.exposure_speed - print("Shutter speed = {}".format(camera.shutter_speed)) + logging.info("Shutter speed = {}".format(camera.shutter_speed)) camera.exposure_mode = "off" - print("Auto exposure disabled") + logging.info("Auto exposure disabled") g = camera.awb_gains camera.awb_mode = "off" camera.awb_gains = g - print("Auto white balance disabled, gains are {}".format(g)) - print("Analogue gain: {}, Digital gain: {}".format(camera.analog_gain, camera.digital_gain)) + logging.info("Auto white balance disabled, gains are {}".format(g)) + logging.info( + "Analogue gain: {}, Digital gain: {}".format( + camera.analog_gain, camera.digital_gain + ) + ) adjust_exposure_to_setpoint(camera, 215) def channels_from_bayer_array(bayer_array): """Given the 'array' from a PiBayerArray, return the 4 channels.""" - bayer_pattern = [(i//2, i % 2) for i in range(4)] - channels = np.zeros((4, bayer_array.shape[0]//2, bayer_array.shape[1]//2), dtype=bayer_array.dtype) + bayer_pattern = [(i // 2, i % 2) for i in range(4)] + channels = np.zeros( + (4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2), + dtype=bayer_array.dtype, + ) for i, offset in enumerate(bayer_pattern): # We simplify life by dealing with only one channel at a time. - channels[i, :, :] = np.sum(bayer_array[offset[0]::2, offset[1]::2, :], axis=2) + channels[i, :, :] = np.sum( + bayer_array[offset[0] :: 2, offset[1] :: 2, :], axis=2 + ) return channels @@ -74,7 +91,7 @@ def lst_from_channels(channels): # lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int)) lst_resolution = [(r // 64) + 1 for r in full_resolution] # NB the size of the LST is 1/64th of the image, but rounded UP. - print("Generating a lens shading table at {}x{}".format(*lst_resolution)) + logging.info("Generating a lens shading table at {}x{}".format(*lst_resolution)) lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float) for i in range(lens_shading.shape[0]): image_channel = channels[i, :, :] @@ -86,25 +103,27 @@ def lst_from_channels(channels): # pad the image by copying edge pixels, so that it is exactly 32 times the # size of the lens shading table (NB 32 not 64 because each channel is only # half the size of the full image - remember the Bayer pattern... This - # should give results very close to 6by9's solution, albeit considerably + # should give results very close to 6by9's solution, albeit considerably # less computationally efficient! - padded_image_channel = np.pad(image_channel, - [(0, lw*32 - iw), (0, lh*32 - ih)], - mode="edge") # Pad image to the right and bottom - print("Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(iw, - ih, - lw*32, - lh*32, - padded_image_channel.shape)) + padded_image_channel = np.pad( + image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge" + ) # Pad image to the right and bottom + logging.info( + "Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format( + iw, ih, lw * 32, lh * 32, padded_image_channel.shape + ) + ) # Next, fill the shading table (except edge pixels). Please excuse the # for loop - I know it's not fast but this code needn't be! box = 3 # We average together a square of this side length for each pixel. # NB this isn't quite what 6by9's program does - it averages 3 pixels # horizontally, but not vertically. - for dx in np.arange(box) - box//2: - for dy in np.arange(box) - box//2: - ls_channel[:, :] += padded_image_channel[16+dx::32, 16+dy::32] - 64 - ls_channel /= box**2 + for dx in np.arange(box) - box // 2: + for dy in np.arange(box) - box // 2: + ls_channel[:, :] += ( + padded_image_channel[16 + dx :: 32, 16 + dy :: 32] - 64 + ) + ls_channel /= box ** 2 # The original C code written by 6by9 normalises to the central 64 pixels in each channel. # ls_channel /= np.mean(image_channel[iw//2-4:iw//2+4, ih//2-4:ih//2+4]) # I have had better results just normalising to the maximum: @@ -114,10 +133,10 @@ def lst_from_channels(channels): # For most sensible lenses I'd expect that 1.0 is the maximum value. # NB ls_channel should be a "view" of the whole lens shading array, so we don't # need to update the big array here. - - # What we actually want to calculate is the gains needed to compensate for the + + # What we actually want to calculate is the gains needed to compensate for the # lens shading - that's 1/lens_shading_table_float as we currently have it. - gains = 32.0/lens_shading # 32 is unity gain + gains = 32.0 / lens_shading # 32 is unity gain gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32 gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?) lens_shading_table = gains.astype(np.uint8) @@ -145,7 +164,7 @@ def recalibrate_camera(camera): # raw_image is a 3D array, with full resolution and 3 colour channels. No # de-mosaicing has been done, so 2/3 of the values are zero (3/4 for R and B # channels, 1/2 for green because there's twice as many green pixels). - channels = channels_from_bayer_array(raw_image) + channels = channels_from_bayer_array(raw_image) lens_shading_table = lst_from_channels(channels) camera.lens_shading_table = lens_shading_table @@ -154,8 +173,10 @@ def recalibrate_camera(camera): # Fix the AWB gains so the image is neutral channel_means = np.mean(np.mean(rgb_image(camera), axis=0, dtype=np.float), axis=0) old_gains = camera.awb_gains - camera.awb_gains = (channel_means[1]/channel_means[0] * old_gains[0], - channel_means[1]/channel_means[2]*old_gains[1]) + camera.awb_gains = ( + channel_means[1] / channel_means[0] * old_gains[0], + channel_means[1] / channel_means[2] * old_gains[1], + ) time.sleep(1) # Ensure the background is bright but not saturated adjust_exposure_to_setpoint(camera, 230) @@ -165,7 +186,7 @@ if __name__ == "__main__": with PiCamera() as camera: camera.start_preview() time.sleep(3) - print("Recalibrating...") + logging.info("Recalibrating...") recalibrate_camera(camera) - print("Done.") + logging.info("Done.") time.sleep(2) diff --git a/openflexure_microscope/api/default_extensions/scan.py b/openflexure_microscope/api/default_extensions/scan.py new file mode 100644 index 00000000..7d510549 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/scan.py @@ -0,0 +1,354 @@ +import itertools +import logging +import uuid +import datetime +from typing import Tuple +from functools import reduce + +from openflexure_microscope.captures.capture_manager import generate_basename + +from labthings import fields, find_component, find_extension, current_action, update_action_progress +from labthings.views import View, ActionView +from labthings.extensions import BaseExtension + +from openflexure_microscope.devel import abort + +import time + + +### Grid construction + + +def construct_grid(initial, step_sizes, n_steps, style="raster"): + """ + Given an initial position, step sizes, and number of steps, + construct a 2-dimensional list of scan x-y positions. + """ + arr = [] + + for i in range(n_steps[0]): # x axis + arr.append([]) + for j in range(n_steps[1]): # y axis + # Create a coordinate array + coord = [initial[ax] + [i, j][ax] * step_sizes[ax] for ax in range(2)] + # Append coordinate array to position grid + arr[i].append(tuple(coord)) + + # Style modifiers + if style == "snake": + for i, line in enumerate(arr): + if i % 2 != 0: + line.reverse() + + return arr + + +def flatten_grid(grid): + """ + Convert a 3D list of scan positions into a flat list + of sequential positions. + """ + + grid = list(itertools.chain(*grid)) + return grid + + +### Capturing + + +def capture( + microscope, + basename, + temporary: bool = False, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + metadata: dict = {}, + annotations: dict = {}, + tags: list = [], +): + + # Construct a tile filename + filename = "{}_{}_{}_{}".format(basename, *microscope.stage.position) + folder = "SCAN_{}".format(basename) + + # Do capture + return microscope.capture( + filename=filename, + folder=folder, + temporary=temporary, + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + annotations=annotations, + tags=tags, + metadata=metadata, + cache_key=folder + ) + + +class ScanExtension(BaseExtension): + def __init__(self): + self._images_to_be_captured: int = 1 + self._images_captured_so_far: int = 0 + BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0") + + def progress(self): + progress = (self._images_captured_so_far / self._images_to_be_captured) * 100 + logging.info(progress) + return progress + + ### Scanning + def tile( + self, + microscope, + basename: str = None, + temporary: bool = False, + stride_size: int = [2000, 1500, 100], + grid: list = [3, 3, 5], + style="raster", + autofocus_dz: int = 50, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + fast_autofocus=False, + metadata: dict = {}, + annotations: dict = {}, + tags: list = [], + ): + start = time.time() + + # Keep task progress + self._images_to_be_captured = reduce((lambda x, y: x * y), grid) + self._images_captured_so_far = 0 + + # Generate a basename if none given + if not basename: + basename = generate_basename() + + # Store initial position + initial_position = microscope.stage.position + + # Add dataset metadata + dataset_d = { + "dataset": { + "id": uuid.uuid4(), + "type": "xyzScan", + "name": basename, + "acquisitionDate": datetime.datetime.now().isoformat(), + "strideSize": stride_size, + "grid": grid, + "style": style, + "autofocusDz": autofocus_dz, + } + } + + # Check if autofocus is enabled + autofocus_extension = find_extension("org.openflexure.autofocus") + if ( + autofocus_dz + and autofocus_extension + and microscope.has_real_stage() + and microscope.has_real_camera() + ): + autofocus_enabled = True + else: + autofocus_enabled = False + + # Construct an x-y grid (worry about z later) + x_y_grid = construct_grid(initial_position, stride_size[:2], grid[:2], style=style) + + # Keep the initial Z position the same as our current position + initial_z = initial_position[2] + next_z = initial_z # Save this value for use in raster scans + + # Now step through each point in the x-y coordinate array + for line in x_y_grid: + # If rastering, rather than snake (or eventually spiral) + # Return focus to initial position + if style == "raster": + next_z = initial_z # Reset z position at start of each new row + logging.debug("Returning to initial z position") + microscope.stage.move_abs( + [line[0][0], line[0][1], next_z] + ) # RWB: I think this line is redundant + + for x_y in line: + # Move to new grid position without changing z + logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z])) + microscope.stage.move_abs([x_y[0], x_y[1], next_z]) + # Refocus + if autofocus_enabled: + if fast_autofocus: + # Run fast autofocus. Client should provide dz ~ 2000 + autofocus_extension.fast_up_down_up_autofocus( + microscope, dz=autofocus_dz + ) + else: + # Run slow autofocus. Client should provide dz ~ 50 + autofocus_extension.autofocus( + microscope, + range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz), + ) + logging.debug("Finished autofocus") + time.sleep(1) + + # If we're not doing a z-stack, just capture + if grid[2] <= 1: + capture( + microscope, + basename, + temporary=temporary, + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + metadata=dataset_d, + annotations=annotations, + tags=tags, + ) + # Update task progress + self._images_captured_so_far += 1 + update_action_progress(self.progress()) + else: + logging.debug("Entering z-stack") + self.stack( + microscope=microscope, + basename=basename, + temporary=temporary, + step_size=stride_size[2], + steps=grid[2], + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + metadata=dataset_d, + annotations=annotations, + tags=tags, + ) + if current_action() and current_action().stopped: + return + # Make sure we use our current best estimate of focus (i.e. the current position) next point + next_z = microscope.stage.position[2] + + logging.debug("Returning to {}".format(initial_position)) + microscope.stage.move_abs(initial_position) + + end = time.time() + logging.info(f"Scan took {end - start} seconds") + + + def stack( + self, + microscope, + basename: str = None, + temporary: bool = False, + step_size: int = 100, + steps: int = 5, + return_to_start: bool = True, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = False, + metadata: dict = {}, + annotations: dict = {}, + tags: list = [], + ): + + # Store initial position + initial_position = microscope.stage.position + logging.debug(f"Starting z-stack from position {microscope.stage.position}") + + with microscope.lock: + # Move to center scan + logging.debug("Moving to z-stack starting position") + microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)]) + logging.debug(f"Starting scan from position {microscope.stage.position}") + + for i in range(steps): + time.sleep(0.1) + logging.debug(f"Capturing from position {microscope.stage.position}") + capture( + microscope, + basename, + temporary=temporary, + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + metadata=metadata, + annotations=annotations, + tags=tags, + ) + # Update task progress + self._images_captured_so_far += 1 + update_action_progress(self.progress()) + if current_action() and current_action().stopped: + return + + if i != steps - 1: + logging.debug("Moving z by {}".format(step_size)) + microscope.stage.move_rel([0, 0, step_size]) + if return_to_start: + logging.debug("Returning to {}".format(initial_position)) + microscope.stage.move_abs(initial_position) + + +scan_extension_v2 = ScanExtension() + +class TileScanAPI(ActionView): + args = { + "filename": fields.String(missing=None, example=None), + "temporary": fields.Boolean(missing=False), + "stride_size": fields.List( + fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100] + ), + "grid": fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]), + "style": fields.String(missing="raster"), + "autofocus_dz": fields.Integer(missing=50), + "fast_autofocus": fields.Boolean(missing=False), + "use_video_port": fields.Boolean(missing=False), + "bayer": fields.Boolean(missing=False), + "annotations": fields.Dict(missing={}, example={"Foo": "Bar"}), + "tags": fields.List(fields.String, missing=[]), + "resize": fields.Dict(missing=None), # TODO: Validate keys + } + + # Allow 10 seconds to stop upon DELETE request + # Gives fast-autofocus time to finish if it's running + default_stop_timeout = 10 + + def post(self, args): + microscope = find_component("org.openflexure.microscope") + + if not microscope: + abort(503, "No microscope connected. Unable to autofocus.") + + resize = args.get("resize", None) + if resize: + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple + else: + abort(404) + + logging.info("Running tile scan...") + + # Acquire microscope lock with 1s timeout + with microscope.lock(timeout=1): + # Run scan_extension_v2 + return scan_extension_v2.tile( + microscope, + basename=args.get("filename"), + temporary=args.get("temporary"), + stride_size=args.get("stride_size"), + grid=args.get("grid"), + style=args.get("style"), + autofocus_dz=args.get("autofocus_dz"), + use_video_port=args.get("use_video_port"), + resize=resize, + bayer=args.get("bayer"), + fast_autofocus=args.get("fast_autofocus"), + annotations=args.get("annotations"), + tags=args.get("tags"), + ) + +scan_extension_v2.add_view(TileScanAPI, "/tile", endpoint="tile") diff --git a/openflexure_microscope/api/default_extensions/zip_builder.py b/openflexure_microscope/api/default_extensions/zip_builder.py new file mode 100644 index 00000000..0445fd90 --- /dev/null +++ b/openflexure_microscope/api/default_extensions/zip_builder.py @@ -0,0 +1,196 @@ +from openflexure_microscope.devel import ( + JsonResponse, + request, +) + +from flask import send_file, abort, url_for + +import uuid +import os +import zipfile +import tempfile +import logging + +from labthings import fields, find_component, update_action_progress +from labthings.views import View, ActionView, PropertyView +from labthings.schema import Schema, pre_dump +from labthings.extensions import BaseExtension +from labthings.utilities import description_from_view + + +class ZipObjectSchema(Schema): + id = fields.String() + data_size = fields.Number() + zip_size = fields.Number() + links = fields.Dict() + + @pre_dump + def generate_links(self, data, **kwargs): + data.links = { + "download": { + "href": url_for( + ZipGetterAPIView.endpoint, session_id=data.id, _external=True + ), + **description_from_view(ZipGetterAPIView), + } + } + return data + + +class ZipObjectDescription: + def __init__(self, id, file_pointer, data_size=None): + self.id = id + self.fp = file_pointer + self.data_size = data_size + self.zip_size = os.path.getsize(self.fp.name) * 1e-6 + + def close(self): + logging.debug(self.fp.name) + self.fp.close() + os.unlink(self.fp.name) + + assert not os.path.exists(self.fp.name) + + def __del__(self): + self.close() + + +class ZipManager: + """ + ZIP-builder manager + """ + + def __init__(self): + super().__init__() + + self.session_zips = {} + + def build_zip_from_capture_ids(self, microscope, capture_id_list): + logging.debug(capture_id_list) + + # Get array of captures from IDs + capture_list = [ + microscope.captures.images.get(capture_id) for capture_id in capture_id_list + ] + # Remove Nones from list (missing/invalid captures) + capture_list = [capture for capture in capture_list if capture] + + # Get size (in bytes) of each capture + capture_sizes = [ + os.path.getsize(capture_obj.file) for capture_obj in capture_list + ] + # Calculate size of input data in megabytes + data_size_megabytes = sum(capture_sizes) * 1e-6 + + # If more than 1GB + if data_size_megabytes > 1000: + # Throw exception + raise Exception( + "Zip data cannot exceed 1GB. Please transfer data manually." + ) + + # Number of files to add (used for task progress) + n_files = len(capture_id_list) + + # Create temporary file + fp = tempfile.NamedTemporaryFile(delete=False) + + # Open temp file as a ZIP file + with zipfile.ZipFile(fp, "w") as zipObj: + for index, capture_obj in enumerate(capture_list): + # Add to ZIP file if it exists + file_path = capture_obj.file + rel_path = os.path.relpath( + file_path, microscope.captures.paths["default"] + ) + zipObj.write(file_path, arcname=rel_path) + # Update task progress + update_action_progress(int((index / n_files) * 100)) + + session_id = str(uuid.uuid4()) + session_description = ZipObjectDescription( + session_id, fp, data_size=data_size_megabytes + ) + self.session_zips[session_id] = session_description + + return self.session_zips[session_id] + + def marshaled_build_zip_from_capture_ids(self, *args, **kwargs): + return ZipObjectSchema().dump(self.build_zip_from_capture_ids(*args, **kwargs)) + + def zip_fp_from_id(self, session_id): + return self.session_zips[session_id].fp + + def __del__(self): + for zd in self.session_zips.values(): + zd.close() + + +# Create a global ZIP manager +default_zip_manager = ZipManager() + + +class ZipBuilderAPIView(ActionView): + def post(self): + ids = list(JsonResponse(request).json) + microscope = find_component("org.openflexure.microscope") + + # Return a handle on the autofocus task + return default_zip_manager.marshaled_build_zip_from_capture_ids( + microscope, ids + ) + + +class ZipListAPIView(PropertyView): + schema = ZipObjectSchema(many=True) + + def get(self): + return default_zip_manager.session_zips.values() + + +class ZipGetterAPIView(View): + """ + Download or delete a particular capture collection ZIP file + """ + + def get(self, session_id): + """ + Download a particular capture collection ZIP file + """ + if not session_id in default_zip_manager.session_zips: + return abort(404) # 404 Not Found + + logging.info(f"Session ID: {session_id}") + + return send_file( + default_zip_manager.zip_fp_from_id(session_id).name, + mimetype="application/zip", + as_attachment=True, + attachment_filename=f"{session_id}.zip", + ) + + def delete(self, session_id): + """ + Close and delete a particular capture collection ZIP file + """ + if not session_id in default_zip_manager.session_zips: + return abort(404) # 404 Not Found + + # Close the file + default_zip_manager.session_zips[session_id].close() + # Delete the file reference + del default_zip_manager.session_zips[session_id] + + return {"return": session_id} + + +zip_extension_v2 = BaseExtension( + "org.openflexure.zipbuilder", + version="2.0.0", + description="Build and download capture collections as ZIP files", +) + +zip_extension_v2.add_view(ZipGetterAPIView, "/get/", endpoint="get_id") +zip_extension_v2.add_view(ZipListAPIView, "/get", endpoint="get") + +zip_extension_v2.add_view(ZipBuilderAPIView, "/build", endpoint="build") diff --git a/openflexure_microscope/api/dev_extensions/__init__.py b/openflexure_microscope/api/dev_extensions/__init__.py new file mode 100644 index 00000000..e951fc64 --- /dev/null +++ b/openflexure_microscope/api/dev_extensions/__init__.py @@ -0,0 +1 @@ +from .tools import devtools_extension_v2 \ No newline at end of file diff --git a/openflexure_microscope/api/dev_extensions/tools.py b/openflexure_microscope/api/dev_extensions/tools.py new file mode 100644 index 00000000..9609c3ff --- /dev/null +++ b/openflexure_microscope/api/dev_extensions/tools.py @@ -0,0 +1,32 @@ +from labthings import fields +from labthings.extensions import BaseExtension +from labthings.views import ActionView + +import logging +import time + +class RaiseException(ActionView): + def post(self): + raise Exception("The developer raised an exception") + +class SleepFor(ActionView): + schema = {"TimeAsleep": fields.Float()} + args = {"time": fields.Float(description="Time to sleep, in seconds", example=0.5)} + + def post(self, args): + sleep_time = args.get("time") + logging.info(f"Going to sleep for {sleep_time}...") + start = time.time() + time.sleep(sleep_time) + end = time.time() + logging.info("Waking up!") + return {"TimeAsleep": (end - start)} + +devtools_extension_v2 = BaseExtension( + "org.openflexure.dev.tools", + version="0.1.0", + description="Actions to cause various traumatic events in the microscope, used for testing.", +) + +devtools_extension_v2.add_view(RaiseException, "/raise") +devtools_extension_v2.add_view(SleepFor, "/sleep") \ No newline at end of file diff --git a/openflexure_microscope/api/exceptions.py b/openflexure_microscope/api/exceptions.py deleted file mode 100644 index d3a8f048..00000000 --- a/openflexure_microscope/api/exceptions.py +++ /dev/null @@ -1,35 +0,0 @@ -from flask import jsonify, escape -from werkzeug.exceptions import default_exceptions -from werkzeug.exceptions import HTTPException - - -class JSONExceptionHandler(object): - - def __init__(self, app=None): - if app: - self.init_app(app) - - def std_handler(self, error): - if isinstance(error, HTTPException): - message = error.description - elif hasattr(error, 'message'): - message = error.message - else: - message = str(error) - - status_code = error.code if isinstance(error, HTTPException) else 500 - - response = { - 'status_code': status_code, - 'message': escape(message) - } - return jsonify(response), status_code - - def init_app(self, app): - self.app = app - self.register(HTTPException) - for code, v in default_exceptions.items(): - self.register(code) - - def register(self, exception_or_code, handler=None): - self.app.errorhandler(exception_or_code)(handler or self.std_handler) diff --git a/openflexure_microscope/api/microscope.py b/openflexure_microscope/api/microscope.py new file mode 100644 index 00000000..87773bd4 --- /dev/null +++ b/openflexure_microscope/api/microscope.py @@ -0,0 +1,12 @@ +from openflexure_microscope.microscope import Microscope + +import logging + + +default_microscope = Microscope() + +# Restore loaded capture array to camera object +logging.debug("Restoring captures...") +default_microscope.captures.rebuild_captures() + +logging.debug("Microscope successfully attached!") diff --git a/openflexure_microscope/api/static/.env.development b/openflexure_microscope/api/static/.env.development new file mode 100644 index 00000000..284c703f --- /dev/null +++ b/openflexure_microscope/api/static/.env.development @@ -0,0 +1,3 @@ +NODE_ENV=development +VUE_APP_PLATFORM=web +VUE_APP_TARGET=web \ No newline at end of file diff --git a/openflexure_microscope/api/static/.env.production b/openflexure_microscope/api/static/.env.production new file mode 100644 index 00000000..e9a62687 --- /dev/null +++ b/openflexure_microscope/api/static/.env.production @@ -0,0 +1,3 @@ +NODE_ENV=production +VUE_APP_PLATFORM=web +VUE_APP_TARGET=web \ No newline at end of file diff --git a/openflexure_microscope/api/static/.eslintrc.js b/openflexure_microscope/api/static/.eslintrc.js new file mode 100644 index 00000000..d1d8c124 --- /dev/null +++ b/openflexure_microscope/api/static/.eslintrc.js @@ -0,0 +1,24 @@ + +module.exports = { + root: true, + env: { + node: true + }, + extends: [ + "plugin:vue/recommended", + "eslint:recommended", + "prettier/vue", + "plugin:prettier/recommended" + ], + rules: { + "vue/component-name-in-template-casing": ["error", "PascalCase"], + "no-console": process.env.NODE_ENV === "production" ? "error" : "off", + "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" + }, + globals: { + $nuxt: true + }, + parserOptions: { + parser: "babel-eslint" + } +}; \ No newline at end of file diff --git a/openflexure_microscope/api/static/.gitignore b/openflexure_microscope/api/static/.gitignore new file mode 100644 index 00000000..2f59b8bf --- /dev/null +++ b/openflexure_microscope/api/static/.gitignore @@ -0,0 +1,30 @@ +.DS_Store +node_modules +/dist +/dist-lite +/app/dist +/release-builds +/installers/release-builds + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw* + +# NativeScript application +hooks +platforms +./webpack.config.js \ No newline at end of file diff --git a/openflexure_microscope/api/static/CONTRIBUTING.md b/openflexure_microscope/api/static/CONTRIBUTING.md new file mode 100644 index 00000000..42d43a66 --- /dev/null +++ b/openflexure_microscope/api/static/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a working flow, and a code of conduct. Please follow it in all your interactions with the project. + +## Flow + + +### Development + +* Master is the latest working version. +* New features and bug fixes should be developed on branches (or forks), and merged into master. + +### Releases + +* Once we've merged a few things in and want to make a release, we tag it on master. +* Not every commit on master will get a tag, we'll accumulate a few changes before making a release, unless there's a reason to release straight away. +* Once we're confident that the tagged release is building and stable, we merge it into the stable branch. + +## Code of Conduct + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/openflexure_microscope/api/static/LICENSE b/openflexure_microscope/api/static/LICENSE new file mode 100644 index 00000000..10537ed5 --- /dev/null +++ b/openflexure_microscope/api/static/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + openflexure-microscope-jsclient + Copyright (C) 2019 OpenFlexure.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + openflexure-microscope-jsclient Copyright (C) 2019 OpenFlexure.org + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/openflexure_microscope/api/static/README.md b/openflexure_microscope/api/static/README.md new file mode 100644 index 00000000..4e5d9005 --- /dev/null +++ b/openflexure_microscope/api/static/README.md @@ -0,0 +1,20 @@ +# OpenFlexure Microscope JS Client + +## Developer notes + +### VS Code and ESLint + +To prevent the editor from interfering with ESLint, add to your project `settings.json`: + +```json +{ + "editor.tabSize": 2, + "cSpell.enabled": false, + "eslint.validate": ["vue","javascript", "javascriptreact"], + "editor.formatOnSave": false, + "vetur.validation.template": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} +``` \ No newline at end of file diff --git a/openflexure_microscope/api/static/babel.config.js b/openflexure_microscope/api/static/babel.config.js new file mode 100644 index 00000000..ba179669 --- /dev/null +++ b/openflexure_microscope/api/static/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/app' + ] +} diff --git a/openflexure_microscope/api/static/package-lock.json b/openflexure_microscope/api/static/package-lock.json new file mode 100644 index 00000000..8e7a763f --- /dev/null +++ b/openflexure_microscope/api/static/package-lock.json @@ -0,0 +1,13812 @@ +{ + "name": "openflexure-microscope-jsclient", + "version": "2.3.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.3.tgz", + "integrity": "sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helpers": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz", + "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.3.tgz", + "integrity": "sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/generator": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.6.tgz", + "integrity": "sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz", + "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz", + "integrity": "sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-call-delegate": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.8.3.tgz", + "integrity": "sha512-6Q05px0Eb+N4/GTyKPPvnkig7Lylw+QzihMpws9iiZQv7ZImf84ZsZpQH7QoWN4n4tm81SnSzPgHw2qtO0Zf3A==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz", + "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.3.tgz", + "integrity": "sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz", + "integrity": "sha512-qmp4pD7zeTxsv0JNecSBsEmG1ei2MqwJq4YQcK3ZWm/0t07QstWfvuV/vm3Qt5xNMFETn2SZqpMx2MQzbtq+KA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz", + "integrity": "sha512-Gcsm1OHCUr9o9TcJln57xhWHtdXbA2pgQ58S0Lxlks0WMGNXuki4+GLfX0p+L2ZkINUGZvfkz8rzoqJQSthI+Q==", + "dev": true, + "requires": { + "@babel/helper-regex": "^7.8.3", + "regexpu-core": "^4.6.0" + } + }, + "@babel/helper-define-map": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz", + "integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/types": "^7.8.3", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz", + "integrity": "sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz", + "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.3.tgz", + "integrity": "sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz", + "integrity": "sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", + "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-module-imports": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", + "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-module-transforms": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz", + "integrity": "sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-simple-access": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz", + "integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==", + "dev": true, + "requires": { + "lodash": "^4.17.13" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz", + "integrity": "sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", + "@babel/helper-wrap-function": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz", + "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.3.tgz", + "integrity": "sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-replace-supers": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz", + "integrity": "sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz", + "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.3.tgz", + "integrity": "sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-simple-access": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", + "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-wrap-function": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", + "integrity": "sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz", + "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.3.tgz", + "integrity": "sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helpers": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.3.tgz", + "integrity": "sha512-LmU3q9Pah/XyZU89QvBgGt+BCsTPoQa+73RxAQh8fb8qkDyIfeQnmgs+hvzhTCKTzqOyk7JTkS3MS1S8Mq5yrQ==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz", + "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.3.tgz", + "integrity": "sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.6.tgz", + "integrity": "sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz", + "integrity": "sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-remap-async-to-generator": "^7.8.3", + "@babel/plugin-syntax-async-generators": "^7.8.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", + "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.8.3.tgz", + "integrity": "sha512-e3RvdvS4qPJVTe288DlXjwKflpfy1hr0j5dz5WpIYYeP7vQZg2WfAEIp8k5/Lwis/m5REXEteIz6rrcDtXXG7w==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-decorators": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz", + "integrity": "sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz", + "integrity": "sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz", + "integrity": "sha512-8Hg4dNNT9/LcA1zQlfwuKR8BUc/if7Q7NkTam9sGTcJphLwpf2g4S42uhspQrIrR+dpzE0dtTqBVFoHl8GtnnQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz", + "integrity": "sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz", + "integrity": "sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz", + "integrity": "sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-remap-async-to-generator": "^7.8.3" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz", + "integrity": "sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz", + "integrity": "sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "lodash": "^4.17.13" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz", + "integrity": "sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", + "@babel/helper-define-map": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz", + "integrity": "sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz", + "integrity": "sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz", + "integrity": "sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz", + "integrity": "sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz", + "integrity": "sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.3.tgz", + "integrity": "sha512-ZjXznLNTxhpf4Q5q3x1NsngzGA38t9naWH8Gt+0qYZEJAcvPI9waSStSh56u19Ofjr7QmD0wUsQ8hw8s/p1VnA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz", + "integrity": "sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.3.tgz", + "integrity": "sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/plugin-transform-literals": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz", + "integrity": "sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz", + "integrity": "sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz", + "integrity": "sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-simple-access": "^7.8.3", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz", + "integrity": "sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.8.3", + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz", + "integrity": "sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz", + "integrity": "sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz", + "integrity": "sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz", + "integrity": "sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.3.tgz", + "integrity": "sha512-/pqngtGb54JwMBZ6S/D3XYylQDFtGjWrnoCF4gXZOUpFV/ujbxnoNGNvDGu6doFWRPBveE72qTx/RRU44j5I/Q==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.8.3", + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz", + "integrity": "sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.8.3.tgz", + "integrity": "sha512-/vqUt5Yh+cgPZXXjmaG9NT8aVfThKk7G4OqkVhrXqwsC5soMn/qTCxs36rZ2QFhpfTJcjw4SNDIZ4RUb8OL4jQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "resolve": "^1.8.1", + "semver": "^5.5.1" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz", + "integrity": "sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz", + "integrity": "sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz", + "integrity": "sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-regex": "^7.8.3" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz", + "integrity": "sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.3.tgz", + "integrity": "sha512-3TrkKd4LPqm4jHs6nPtSDI/SV9Cm5PRJkHLUgTcqRQQTMAZ44ZaAdDZJtvWFSaRcvT0a1rTmJ5ZA5tDKjleF3g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz", + "integrity": "sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/preset-env": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.3.4.tgz", + "integrity": "sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.3.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.2.0", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.3.4", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.3.4", + "@babel/plugin-transform-classes": "^7.3.4", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.2.0", + "@babel/plugin-transform-dotall-regex": "^7.2.0", + "@babel/plugin-transform-duplicate-keys": "^7.2.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.2.0", + "@babel/plugin-transform-function-name": "^7.2.0", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.2.0", + "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "@babel/plugin-transform-modules-systemjs": "^7.3.4", + "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.3.0", + "@babel/plugin-transform-new-target": "^7.0.0", + "@babel/plugin-transform-object-super": "^7.2.0", + "@babel/plugin-transform-parameters": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.3.4", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.2.0", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.2.0", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.2.0", + "browserslist": "^4.3.4", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.3.0" + } + }, + "@babel/runtime": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", + "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/runtime-corejs2": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.8.3.tgz", + "integrity": "sha512-yxJXBKdIogkfF+wgeJrvU7Afp5ugBi92NzSgNPWWKVoQAlixH3gwMP6yYYr7SV1Dbc0HmNw7WUJkV5ksvtQuHg==", + "dev": true, + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + } + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + } + } + }, + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@hapi/address": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", + "dev": true + }, + "@hapi/bourne": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", + "dev": true + }, + "@hapi/hoek": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", + "dev": true + }, + "@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "dev": true, + "requires": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "dev": true, + "requires": { + "@hapi/hoek": "^8.3.0" + } + }, + "@intervolga/optimize-cssnano-plugin": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz", + "integrity": "sha512-zN69TnSr0viRSU6cEDIcuPcP67QcpQ6uHACg58FiN9PDrU6SLyGW3MR4tiISbYxy1kDWAVPwD+XwQTWE5cigAA==", + "dev": true, + "requires": { + "cssnano": "^4.0.0", + "cssnano-preset-default": "^4.0.0", + "postcss": "^7.0.0" + } + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, + "@soda/friendly-errors-webpack-plugin": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz", + "integrity": "sha512-cWKrGaFX+rfbMrAxVv56DzhPNqOJPZuNIS2HGMELtgGzb+vsMzyig9mml5gZ/hr2BGtSLV+dP2LUEuAL8aG2mQ==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "error-stack-parser": "^2.0.0", + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "@soda/get-current-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@soda/get-current-script/-/get-current-script-1.0.0.tgz", + "integrity": "sha512-9GvTek+7cVw7r+L7TNGOG1astZJWXz2h5q4BqMXl28KN+24iSCm1xo+RhZOZvwdT3bzNe9hD7riJc/lBoO7mgg==", + "dev": true + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz", + "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==", + "dev": true + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/q": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", + "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", + "dev": true + }, + "@vue/babel-helper-vue-jsx-merge-props": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz", + "integrity": "sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw==", + "dev": true + }, + "@vue/babel-plugin-transform-vue-jsx": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz", + "integrity": "sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", + "html-tags": "^2.0.0", + "lodash.kebabcase": "^4.1.1", + "svg-tags": "^1.0.0" + } + }, + "@vue/babel-preset-app": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@vue/babel-preset-app/-/babel-preset-app-3.12.1.tgz", + "integrity": "sha512-Zjy5jQaikV1Pz+ri0YgXFS7q4/5wCxB5tRkDOEIt5+4105u0Feb/pvH20nVL6nx9GyXrECFfcm7Yxr/z++OaPQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.0.0", + "@babel/plugin-proposal-decorators": "^7.1.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.4.0", + "@babel/preset-env": "^7.0.0 < 7.4.0", + "@babel/runtime": "^7.0.0", + "@babel/runtime-corejs2": "^7.2.0", + "@vue/babel-preset-jsx": "^1.0.0", + "babel-plugin-dynamic-import-node": "^2.2.0", + "babel-plugin-module-resolver": "3.2.0", + "core-js": "^2.6.5" + } + }, + "@vue/babel-preset-jsx": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.1.2.tgz", + "integrity": "sha512-zDpVnFpeC9YXmvGIDSsKNdL7qCG2rA3gjywLYHPCKDT10erjxF4U+6ay9X6TW5fl4GsDlJp9bVfAVQAAVzxxvQ==", + "dev": true, + "requires": { + "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", + "@vue/babel-sugar-functional-vue": "^1.1.2", + "@vue/babel-sugar-inject-h": "^1.1.2", + "@vue/babel-sugar-v-model": "^1.1.2", + "@vue/babel-sugar-v-on": "^1.1.2" + } + }, + "@vue/babel-sugar-functional-vue": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.1.2.tgz", + "integrity": "sha512-YhmdJQSVEFF5ETJXzrMpj0nkCXEa39TvVxJTuVjzvP2rgKhdMmQzlJuMv/HpadhZaRVMCCF3AEjjJcK5q/cYzQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-inject-h": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.1.2.tgz", + "integrity": "sha512-VRSENdTvD5htpnVp7i7DNuChR5rVMcORdXjvv5HVvpdKHzDZAYiLSD+GhnhxLm3/dMuk8pSzV+k28ECkiN5m8w==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@vue/babel-sugar-v-model": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.1.2.tgz", + "integrity": "sha512-vLXPvNq8vDtt0u9LqFdpGM9W9IWDmCmCyJXuozlq4F4UYVleXJ2Fa+3JsnTZNJcG+pLjjfnEGHci2339Kj5sGg==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", + "camelcase": "^5.0.0", + "html-tags": "^2.0.0", + "svg-tags": "^1.0.0" + } + }, + "@vue/babel-sugar-v-on": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.1.2.tgz", + "integrity": "sha512-T8ZCwC8Jp2uRtcZ88YwZtZXe7eQrJcfRq0uTFy6ShbwYJyz5qWskRFoVsdTi9o0WEhmQXxhQUewodOSCUPVmsQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", + "camelcase": "^5.0.0" + } + }, + "@vue/cli-plugin-babel": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-babel/-/cli-plugin-babel-3.12.1.tgz", + "integrity": "sha512-Zetvz8PikLCGomeKOKu8pC9YQ7cfxs7pGpvEOzaxGdhMnebhjAYR6i6dOB57A6N5lhxQksXCtYTv26QgfiIpdg==", + "dev": true, + "requires": { + "@babel/core": "^7.0.0", + "@vue/babel-preset-app": "^3.12.1", + "@vue/cli-shared-utils": "^3.12.1", + "babel-loader": "^8.0.5", + "webpack": "^4.0.0" + } + }, + "@vue/cli-plugin-vuex": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.4.1.tgz", + "integrity": "sha512-FtOFsDP0qznwVaCz0BZmTzUm5vhHSJzX2/XD3L5dLTkrNxyDEbZmbKoX0n1OzBcQwZC7dkJZP2tdoCQx0mX//g==", + "dev": true + }, + "@vue/cli-service": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.4.1.tgz", + "integrity": "sha512-DVV0zr5Sw7pzdm3z3PRrfqihLgoJP/d9AgNFcSSQF/J9Gtvjf1t0PTJJFeLANHSL3kDWte+3kjc22sXayu0BJQ==", + "dev": true, + "requires": { + "@intervolga/optimize-cssnano-plugin": "^1.0.5", + "@soda/friendly-errors-webpack-plugin": "^1.7.1", + "@soda/get-current-script": "^1.0.0", + "@vue/cli-overlay": "^4.4.1", + "@vue/cli-plugin-router": "^4.4.1", + "@vue/cli-plugin-vuex": "^4.4.1", + "@vue/cli-shared-utils": "^4.4.1", + "@vue/component-compiler-utils": "^3.1.2", + "@vue/preload-webpack-plugin": "^1.1.0", + "@vue/web-component-wrapper": "^1.2.0", + "acorn": "^7.2.0", + "acorn-walk": "^7.1.1", + "address": "^1.1.2", + "autoprefixer": "^9.8.0", + "browserslist": "^4.12.0", + "cache-loader": "^4.1.0", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "cli-highlight": "^2.1.4", + "clipboardy": "^2.3.0", + "cliui": "^6.0.0", + "copy-webpack-plugin": "^5.1.1", + "css-loader": "^3.5.3", + "cssnano": "^4.1.10", + "debug": "^4.1.1", + "default-gateway": "^5.0.5", + "dotenv": "^8.2.0", + "dotenv-expand": "^5.1.0", + "file-loader": "^4.2.0", + "fs-extra": "^7.0.1", + "globby": "^9.2.0", + "hash-sum": "^2.0.0", + "html-webpack-plugin": "^3.2.0", + "launch-editor-middleware": "^2.2.1", + "lodash.defaultsdeep": "^4.6.1", + "lodash.mapvalues": "^4.6.0", + "lodash.transform": "^4.6.0", + "mini-css-extract-plugin": "^0.9.0", + "minimist": "^1.2.5", + "pnp-webpack-plugin": "^1.6.4", + "portfinder": "^1.0.26", + "postcss-loader": "^3.0.0", + "ssri": "^7.1.0", + "terser-webpack-plugin": "^2.3.6", + "thread-loader": "^2.1.3", + "url-loader": "^2.2.0", + "vue-loader": "^15.9.2", + "vue-style-loader": "^4.1.2", + "webpack": "^4.0.0", + "webpack-bundle-analyzer": "^3.8.0", + "webpack-chain": "^6.4.0", + "webpack-dev-server": "^3.11.0", + "webpack-merge": "^4.2.2" + }, + "dependencies": { + "@vue/cli-overlay": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.4.1.tgz", + "integrity": "sha512-EQqAVy7O/qqGOfSYIGL073FWlr/s6QFA0wA1wY8pHnTS5WPwAiHT+D+xe+fgXKZ3KeL7v7u/le7YFIEVXFVXOg==", + "dev": true + }, + "@vue/cli-plugin-router": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.4.1.tgz", + "integrity": "sha512-kCSsJG7pjDvCJDjGtcCI5l0UjmqwNigOR41RkeGSjSUvzV4ArSniXjFqrOmtpMp36S5xCtwtt9MFm/K4fCubkQ==", + "dev": true, + "requires": { + "@vue/cli-shared-utils": "^4.4.1" + } + }, + "@vue/cli-shared-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.4.1.tgz", + "integrity": "sha512-teevHgI7XUsKVMOncx3M+6iLjO28woGfRwgUG4hR83moVBHQe5x2OCr2i5t/58bwpv269RD5RYXBQCGtIXuxZw==", + "dev": true, + "requires": { + "@hapi/joi": "^15.0.1", + "chalk": "^2.4.2", + "execa": "^1.0.0", + "launch-editor": "^2.2.1", + "lru-cache": "^5.1.1", + "node-ipc": "^9.1.1", + "open": "^6.3.0", + "ora": "^3.4.0", + "read-pkg": "^5.1.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.8", + "semver": "^6.1.0", + "strip-ansi": "^6.0.0" + } + }, + "acorn": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "dev": true + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "browserslist": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz", + "integrity": "sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001043", + "electron-to-chromium": "^1.3.413", + "node-releases": "^1.1.53", + "pkg-up": "^2.0.0" + } + }, + "cacache": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", + "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", + "dev": true, + "requires": { + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "p-map": "^3.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^2.7.1", + "ssri": "^7.0.0", + "unique-filename": "^1.1.1" + } + }, + "caniuse-lite": { + "version": "1.0.30001066", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz", + "integrity": "sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "electron-to-chromium": { + "version": "1.3.453", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.453.tgz", + "integrity": "sha512-IQbCfjJR0NDDn/+vojTlq7fPSREcALtF8M1n01gw7nQghCtfFYrJ2dfhsp8APr8bANoFC8vRTFVXMOGpT0eetw==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "node-releases": { + "version": "1.1.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.57.tgz", + "integrity": "sha512-ZQmnWS7adi61A9JsllJ2gdj2PauElcjnOwTp2O011iGzoakTxUsDGSe+6vD7wXbKdqhSFymC0OSx35aAMhrSdw==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "schema-utils": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz", + "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "serialize-javascript": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", + "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "ssri": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz", + "integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "minipass": "^3.1.1" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "terser": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", + "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + } + }, + "terser-webpack-plugin": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.6.tgz", + "integrity": "sha512-I8IDsQwZrqjdmOicNeE8L/MhwatAap3mUrtcAKJuilsemUNcX+Hier/eAzwStVqhlCxq0aG3ni9bK/0BESXkTg==", + "dev": true, + "requires": { + "cacache": "^13.0.1", + "find-cache-dir": "^3.3.1", + "jest-worker": "^25.4.0", + "p-limit": "^2.3.0", + "schema-utils": "^2.6.6", + "serialize-javascript": "^3.0.0", + "source-map": "^0.6.1", + "terser": "^4.6.12", + "webpack-sources": "^1.4.3" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "@vue/cli-shared-utils": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-3.12.1.tgz", + "integrity": "sha512-jFblzRFjutGwu5utOKdVlPlsbA1lBUNNQlAThzNqej+JtTKJjnvjlhjKX0Gq0oOny5FjKWhoyfQ74p9h1qE6JQ==", + "dev": true, + "requires": { + "@hapi/joi": "^15.0.1", + "chalk": "^2.4.1", + "execa": "^1.0.0", + "launch-editor": "^2.2.1", + "lru-cache": "^5.1.1", + "node-ipc": "^9.1.1", + "open": "^6.3.0", + "ora": "^3.4.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.7", + "semver": "^6.0.0", + "string.prototype.padstart": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@vue/component-compiler-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.1.2.tgz", + "integrity": "sha512-QLq9z8m79mCinpaEeSURhnNCN6djxpHw0lpP/bodMlt5kALfONpryMthvnrQOlTcIKoF+VoPi+lPHUYeDFPXug==", + "dev": true, + "requires": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.14", + "postcss-selector-parser": "^6.0.2", + "prettier": "^1.18.2", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "@vue/preload-webpack-plugin": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.1.tgz", + "integrity": "sha512-8VCoJeeH8tCkzhkpfOkt+abALQkS11OIHhte5MBzYaKMTqK0A3ZAKEUVAffsOklhEv7t0yrQt696Opnu9oAx+w==", + "dev": true + }, + "@vue/web-component-wrapper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.2.0.tgz", + "integrity": "sha512-Xn/+vdm9CjuC9p3Ae+lTClNutrVhsXpzxvoTXXtoys6kVRX9FkueSUAqSWAyZntmVLlR4DosBV4pH8y5Z/HbUw==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "dev": true + }, + "acorn-walk": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", + "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==", + "dev": true + }, + "address": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", + "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", + "dev": true + }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "arch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz", + "integrity": "sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz", + "integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==", + "dev": true, + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001061", + "chalk": "^2.4.2", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.30", + "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "browserslist": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz", + "integrity": "sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001043", + "electron-to-chromium": "^1.3.413", + "node-releases": "^1.1.53", + "pkg-up": "^2.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001066", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz", + "integrity": "sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.453", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.453.tgz", + "integrity": "sha512-IQbCfjJR0NDDn/+vojTlq7fPSREcALtF8M1n01gw7nQghCtfFYrJ2dfhsp8APr8bANoFC8vRTFVXMOGpT0eetw==", + "dev": true + }, + "node-releases": { + "version": "1.1.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.57.tgz", + "integrity": "sha512-ZQmnWS7adi61A9JsllJ2gdj2PauElcjnOwTp2O011iGzoakTxUsDGSe+6vD7wXbKdqhSFymC0OSx35aAMhrSdw==", + "dev": true + }, + "postcss": { + "version": "7.0.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.31.tgz", + "integrity": "sha512-a937VDHE1ftkjk+8/7nj/mrjtmkn69xxzJgRETXdAUU+IgOYPQNJF17haGWbeDxSyk++HA14UA98FurvPyBJOA==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "dev": true, + "requires": { + "follow-redirects": "1.5.10" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "babel-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "dev": true, + "requires": { + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-module-resolver": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-3.2.0.tgz", + "integrity": "sha512-tjR0GvSndzPew/Iayf4uICWZqjBwnlMWjSx6brryfQ81F9rxBVqwDJtFCV8oOs0+vJeefK9TmdZtkIFdFe1UnA==", + "dev": true, + "requires": { + "find-babel-config": "^1.1.0", + "glob": "^7.1.2", + "pkg-up": "^2.0.0", + "reselect": "^3.0.1", + "resolve": "^1.4.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bfj": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", + "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "check-types": "^8.0.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz", + "integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + }, + "dependencies": { + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "browserify-sign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz", + "integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==", + "dev": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.2", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.3.tgz", + "integrity": "sha512-iU43cMMknxG1ClEZ2MDKeonKE1CCrFVkQK2AqO2YWFmvIrx4JWrvQ4w4hQez6EpVI8rHTtqh/ruHHDHSOKxvUg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001017", + "electron-to-chromium": "^1.3.322", + "node-releases": "^1.1.44" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-json/-/buffer-json-2.0.0.tgz", + "integrity": "sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cacache": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cache-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-4.1.0.tgz", + "integrity": "sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==", + "dev": true, + "requires": { + "buffer-json": "^2.0.0", + "find-cache-dir": "^3.0.0", + "loader-utils": "^1.2.3", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "schema-utils": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz", + "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001020", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001020.tgz", + "integrity": "sha512-yWIvwA68wRHKanAVS1GjN8vajAv7MBFshullKCeq/eKpK7pJBVDgFFEqvgWTkcP2+wIDeQGYFRXECjKZnLkUjA==", + "dev": true + }, + "case-sensitive-paths-webpack-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz", + "integrity": "sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-types": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==", + "dev": true + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "chownr": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-highlight": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.4.tgz", + "integrity": "sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "highlight.js": "^9.6.0", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^5.1.1", + "yargs": "^15.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cli-spinners": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz", + "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==", + "dev": true + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "dev": true, + "requires": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + }, + "dependencies": { + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + } + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "dev": true, + "requires": { + "bluebird": "^3.1.1" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz", + "integrity": "sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==", + "dev": true, + "requires": { + "cacache": "^12.0.3", + "find-cache-dir": "^2.1.0", + "glob-parent": "^3.1.0", + "globby": "^7.1.1", + "is-glob": "^4.0.1", + "loader-utils": "^1.2.3", + "minimatch": "^3.0.4", + "normalize-path": "^3.0.0", + "p-limit": "^2.2.1", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-loader": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.5.3.tgz", + "integrity": "sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.27", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.3", + "schema-utils": "^2.6.6", + "semver": "^6.3.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "schema-utils": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz", + "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-what": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", + "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz", + "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.39" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", + "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", + "dev": true, + "requires": { + "mdn-data": "2.0.6", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", + "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true + }, + "default-gateway": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-5.0.5.tgz", + "integrity": "sha512-z2RnruVmj8hVMmAnEJMTIJNijhKCDiGjbLP+BHJFOT7ld3Bo5qcIBpVYDniqhbMIIf+jZDlkP2MkPXiQy/DBLA==", + "dev": true, + "requires": { + "execa": "^3.3.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-js": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/dns-js/-/dns-js-0.2.1.tgz", + "integrity": "sha1-XWZimzwOal6w4U8K5wHQX26kZnM=", + "dev": true, + "requires": { + "debug": "^2.1.0", + "qap": "^3.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "easy-stack": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.0.tgz", + "integrity": "sha1-EskbMIWjfwuqM26UhurEv5Tj54g=", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.333", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.333.tgz", + "integrity": "sha512-7MJfCpa/tmhqYy2lZ1NEbkSxH7q3KiZiepiSs2ayTPAweAjdzGXotij+7OKPPb3OwJD2ZuBKPrA2HIuuSi6ahw==", + "dev": true + }, + "elliptic": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", + "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", + "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } + } + }, + "entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz", + "integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "error-stack-parser": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz", + "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==", + "dev": true, + "requires": { + "stackframe": "^1.1.1" + } + }, + "es-abstract": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", + "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", + "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "figures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", + "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "inquirer": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.3.tgz", + "integrity": "sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rxjs": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", + "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", + "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz", + "integrity": "sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-vue": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz", + "integrity": "sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ==", + "dev": true, + "requires": { + "natural-compare": "^1.4.0", + "semver": "^5.6.0", + "vue-eslint-parser": "^7.0.0" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "dev": true, + "requires": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", + "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==", + "dev": true + }, + "events": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "dev": true + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", + "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.5.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "schema-utils": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz", + "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + } + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-babel-config": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.2.0.tgz", + "integrity": "sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==", + "dev": true, + "requires": { + "json5": "^0.5.1", + "path-exists": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", + "integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==", + "dev": true, + "requires": { + "debug": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", + "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1", + "node-pre-gyp": "*" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "3.2.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.9.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.14.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.7.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.1", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.13", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "highlight.js": { + "version": "9.18.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz", + "integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "html-entities": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==", + "dev": true + }, + "html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "dependencies": { + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + } + } + }, + "html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos=", + "dev": true + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "dependencies": { + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + } + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + }, + "dependencies": { + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + } + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "javascript-stringify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.0.1.tgz", + "integrity": "sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow==", + "dev": true + }, + "jest-worker": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", + "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, + "js-message": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz", + "integrity": "sha1-IwDSSxrwjondCVvBpMnJz8uJLRU=", + "dev": true + }, + "js-queue": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.0.tgz", + "integrity": "sha1-NiITz4YPRo8BJfxslqvBdCUx+Ug=", + "dev": true, + "requires": { + "easy-stack": "^1.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jump.js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jump.js/-/jump.js-1.0.2.tgz", + "integrity": "sha1-4GQbR/QKOPITnCX9oFAL8o5DAVo=", + "dev": true + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "launch-editor": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz", + "integrity": "sha512-On+V7K2uZK6wK7x691ycSUbLD/FyKKelArkbaAMSSJU8JmqmhwN2+mnJDNINuJWSrh2L0kDk+ZQtbC/gOWUwLw==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "shell-quote": "^1.6.1" + } + }, + "launch-editor-middleware": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/launch-editor-middleware/-/launch-editor-middleware-2.2.1.tgz", + "integrity": "sha512-s0UO2/gEGiCgei3/2UN3SMuUj1phjQN8lcpnvgLSz26fAzNWPQ6Nf/kF5IFClnfU2ehp6LrmKdMU/beveO+2jg==", + "dev": true, + "requires": { + "launch-editor": "^2.2.1" + } + }, + "less": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/less/-/less-3.11.2.tgz", + "integrity": "sha512-ed8Lir98Tu6a+LeU7+8ShpRLSUdk//lWf1sh+5w7tNju4wGItztqDHp03Z+a2o1nzU6pObVxw1n4Gu7VzQYusQ==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "promise": "^7.1.1", + "request": "^2.83.0", + "source-map": "~0.6.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + } + } + }, + "less-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-5.0.0.tgz", + "integrity": "sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "loader-utils": "^1.1.0", + "pify": "^4.0.1" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + } + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", + "dev": true + }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.transform": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", + "integrity": "sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "loglevel": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", + "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "material-design-icons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", + "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "mdns-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdns-js/-/mdns-js-1.0.3.tgz", + "integrity": "sha512-+6NHS48WZ7na7jkE9PB9dRbBGvY0FvAp8nTGp3/u/05WIyq/B37OVfMppIbHyoo9D4yocJGax4Krxfz3nU7EbQ==", + "dev": true, + "requires": { + "debug": "~3.1.0", + "dns-js": "~0.2.1", + "semver": "~5.4.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "mime": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.5.tgz", + "integrity": "sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", + "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz", + "integrity": "sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mousetrap": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", + "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==" + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "node-forge": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", + "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "dev": true + }, + "node-ipc": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz", + "integrity": "sha512-FAyICv0sIRJxVp3GW5fzgaf9jwwRQxAKDJlmNFUL5hOy+W4X/I5AypyHoq0DXXbo9o/gt79gj++4cMr4jVWE/w==", + "dev": true, + "requires": { + "event-pubsub": "4.3.0", + "js-message": "1.0.5", + "js-queue": "2.0.0" + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "node-releases": { + "version": "1.1.45", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.45.tgz", + "integrity": "sha512-cXvGSfhITKI8qsV116u2FTzH5EWZJfgG7d4cpqwF8I8+1tWpD6AsvvGRKq2onR0DNj1jfqsjkXZsm14JMS7Cyg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-is": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + } + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "opener": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", + "dev": true + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "^2.2.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + } + } + }, + "parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "parse5-htmlparser2-tree-adapter": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz", + "integrity": "sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw==", + "dev": true, + "requires": { + "parse5": "^5.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "optional": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + } + } + }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "pnp-webpack-plugin": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", + "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", + "dev": true, + "requires": { + "ts-pnp": "^1.1.6" + } + }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "dev": true + }, + "portfinder": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", + "integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", + "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz", + "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==", + "dev": true, + "requires": { + "postcss": "^7.0.27", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-load-config": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", + "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + } + }, + "postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz", + "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true, + "optional": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "^2.0.1", + "utila": "~0.4" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "~2.0.3" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", + "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qap": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/qap/-/qap-3.3.1.tgz", + "integrity": "sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "dev": true + }, + "regenerator-transform": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", + "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "regexpu-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", + "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "regjsgen": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", + "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "dev": true + }, + "regjsparser": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.2.tgz", + "integrity": "sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz", + "integrity": "sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==", + "dev": true, + "requires": { + "css-select": "^1.1.0", + "dom-converter": "^0.2", + "htmlparser2": "^3.3.0", + "strip-ansi": "^3.0.0", + "utila": "^0.4.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "dev": true, + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "reselect": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz", + "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=", + "dev": true + }, + "resolve": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", + "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", + "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "dev": true, + "requires": { + "node-forge": "0.9.0" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", + "dev": true, + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "sockjs-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", + "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", + "dev": true, + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "stackframe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.1.1.tgz", + "integrity": "sha512-0PlYhdKh6AfFxRyK/v+6/k+/mMfyiEBbTM5L94D0ZytQnJ166wuwoTYLHFWGbs2dpA8Rgq763KGWmN1EQEYHRQ==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string.prototype.padstart": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.padstart/-/string.prototype.padstart-3.1.0.tgz", + "integrity": "sha512-envqZvUp2JItI+OeQ5UAh1ihbAV5G/2bixTojvlIa090GGqF+NQRxbWb2nv9fTGrZABv6+pE6jXoAZhhS2k4Hw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", + "dev": true + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terser": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", + "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", + "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "thenify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "dev": true, + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "dev": true, + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "thread-loader": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-2.1.3.tgz", + "integrity": "sha512-wNrVKH2Lcf8ZrWxDF/khdlLlsTMczdcwPA9VEK4c2exlEPynYWxi9op3nPTo5lAnDIkE0rQEB3VBP+4Zncc9Hg==", + "dev": true, + "requires": { + "loader-runner": "^2.3.1", + "loader-utils": "^1.1.0", + "neo-async": "^2.6.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true + }, + "ts-pnp": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", + "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", + "dev": true + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", + "dev": true, + "requires": { + "commander": "~2.19.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "uikit": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/uikit/-/uikit-3.5.3.tgz", + "integrity": "sha512-p3F7ICtq0XNYbTcrbOotjmdOAf8D7F8kbnZOIHomUK0fCVn8BdFPscRWzO/9cerxGw4eY7R7//BZupoyEYg2Fg==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz", + "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "mime": "^2.4.4", + "schema-utils": "^2.5.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "schema-utils": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz", + "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + } + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "vue": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz", + "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==", + "dev": true + }, + "vue-eslint-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.0.0.tgz", + "integrity": "sha512-yR0dLxsTT7JfD2YQo9BhnQ6bUTLsZouuzt9SKRP7XNaZJV459gvlsJo4vT2nhZ/2dH9j3c53bIx9dnqU2prM9g==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-scope": "^5.0.0", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "lodash": "^4.17.15" + }, + "dependencies": { + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, + "vue-friendly-iframe": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/vue-friendly-iframe/-/vue-friendly-iframe-0.17.0.tgz", + "integrity": "sha512-2IEHRpz2KIZ7wSXd3h1n6EEQ9HEU6GRC4DNdQ7w5Dm31Udf/xGjWfHErXJUsUw3Qal0fga8SrA6aOPTOX7mg+Q==", + "dev": true, + "requires": { + "uuid": "3.1.0" + }, + "dependencies": { + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + } + } + }, + "vue-hot-reload-api": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", + "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", + "dev": true + }, + "vue-loader": { + "version": "15.9.2", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.2.tgz", + "integrity": "sha512-oXBubaY//CYEISBlHX+c2YPJbmOH68xXPXjFv4MAgPqQvUsnjrBAjCJi8HXZ/r/yfn0tPL5VZj1Zcp8mJPI8VA==", + "dev": true, + "requires": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", + "dev": true + } + } + }, + "vue-plugin-load-script": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vue-plugin-load-script/-/vue-plugin-load-script-1.2.0.tgz", + "integrity": "sha512-QbWjZSFRToSP6S0nZFsH618PsTlZgSg8m8Xv602vezznLTHYd0wAXEw0jlYND7L6BMe0KcA7cvUwLROyfBlQ4w==", + "dev": true + }, + "vue-style-loader": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", + "integrity": "sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ==", + "dev": true, + "requires": { + "hash-sum": "^1.0.2", + "loader-utils": "^1.0.2" + }, + "dependencies": { + "hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=", + "dev": true + } + } + }, + "vue-template-compiler": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz", + "integrity": "sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==", + "dev": true, + "requires": { + "de-indent": "^1.0.2", + "he": "^1.1.0" + } + }, + "vue-template-es2015-compiler": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", + "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", + "dev": true + }, + "vue-tour": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/vue-tour/-/vue-tour-1.4.0.tgz", + "integrity": "sha512-vsuxNLsSSKBOJpDme1ZkcY744LYk+l4TSfQz39xJZ4BE1GrioyTzS4+PT3D1nKhT/3vOZurNM66cZgzgRNZ/wA==", + "dev": true, + "requires": { + "hash-sum": "^2.0.0", + "jump.js": "^1.0.2", + "popper.js": "^1.16.0", + "vue": "^2.6.10" + } + }, + "vuex": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.4.0.tgz", + "integrity": "sha512-ajtqwEW/QhnrBZQsZxCLHThZZaa+Db45c92Asf46ZDXu6uHXgbfVuBaJ4gzD2r4UX0oMJHstFwd2r2HM4l8umg==", + "dev": true + }, + "watchpack": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz", + "integrity": "sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==", + "dev": true, + "requires": { + "chokidar": "^3.4.0", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true, + "optional": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", + "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "watchpack-chokidar2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", + "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webpack": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz", + "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.6.1", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + } + } + }, + "webpack-bundle-analyzer": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.8.0.tgz", + "integrity": "sha512-PODQhAYVEourCcOuU+NiYI7WdR8QyELZGgPvB1y2tjbUpbmcQOt5Q7jEK+ttd5se0KSBKD9SXHCEozS++Wllmw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1", + "bfj": "^6.1.1", + "chalk": "^2.4.1", + "commander": "^2.18.0", + "ejs": "^2.6.1", + "express": "^4.16.3", + "filesize": "^3.6.1", + "gzip-size": "^5.0.0", + "lodash": "^4.17.15", + "mkdirp": "^0.5.1", + "opener": "^1.5.1", + "ws": "^6.0.0" + }, + "dependencies": { + "acorn": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "dev": true + } + } + }, + "webpack-chain": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-6.4.0.tgz", + "integrity": "sha512-f97PYqxU+9/u0IUqp/ekAHRhBD1IQwhBv3wlJo2nvyELpr2vNnUqO3XQEk+qneg0uWGP54iciotszpjfnEExFA==", + "dev": true, + "requires": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + } + }, + "webpack-dev-middleware": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", + "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + } + }, + "webpack-dev-server": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.7", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "0.3.20", + "sockjs-client": "1.4.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "dev": true, + "requires": { + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/openflexure_microscope/api/static/package.json b/openflexure_microscope/api/static/package.json new file mode 100644 index 00000000..af5be7b0 --- /dev/null +++ b/openflexure_microscope/api/static/package.json @@ -0,0 +1,50 @@ +{ + "name": "openflexure-microscope-jsclient", + "version": "2.4.0", + "private": true, + "description": "User client for the OpenFlexure Microscope Server", + "author": "OpenFlexure (https://www.openflexure.org)", + "homepage": "https://gitlab.com/openflexure/openflexure-microscope-jsclient", + "publisher": "Bath Open Instrumentation Group", + "displayName": "OpenFlexure Microscope", + "license": "GNU General Public License v3.0 ", + "scripts": { + "build": "vue-cli-service build --mode production", + "serve": "vue-cli-service serve --mode development" + }, + "dependencies": { + "material-design-icons": "^3.0.1", + "mousetrap": "^1.6.5" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^3.12.1", + "@vue/cli-service": "^4.4.1", + "axios": "^0.19.2", + "babel-eslint": "^10.1.0", + "css-loader": "^3.5.3", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.3", + "eslint-plugin-vue": "^6.2.2", + "less": "^3.11.2", + "less-loader": "^5.0.0", + "mdns-js": "^1.0.3", + "uikit": "^3.5.3", + "vue": "^2.6.11", + "vue-friendly-iframe": "^0.17.0", + "vue-plugin-load-script": "^1.2.0", + "vue-template-compiler": "^2.6.11", + "vue-tour": "^1.4.0", + "vuex": "^3.4.0" + }, + "postcss": { + "plugins": { + "autoprefixer": {} + } + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/openflexure_microscope/api/static/public/favicon-16x16.png b/openflexure_microscope/api/static/public/favicon-16x16.png new file mode 100644 index 00000000..98412f55 Binary files /dev/null and b/openflexure_microscope/api/static/public/favicon-16x16.png differ diff --git a/openflexure_microscope/api/static/public/favicon-32x32.png b/openflexure_microscope/api/static/public/favicon-32x32.png new file mode 100644 index 00000000..1c5fce78 Binary files /dev/null and b/openflexure_microscope/api/static/public/favicon-32x32.png differ diff --git a/openflexure_microscope/api/static/public/favicon.ico b/openflexure_microscope/api/static/public/favicon.ico new file mode 100644 index 00000000..fca78375 Binary files /dev/null and b/openflexure_microscope/api/static/public/favicon.ico differ diff --git a/openflexure_microscope/api/static/public/index.html b/openflexure_microscope/api/static/public/index.html new file mode 100644 index 00000000..375edb52 --- /dev/null +++ b/openflexure_microscope/api/static/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + OpenFlexure Microscope + + + +
+ + + diff --git a/openflexure_microscope/api/static/public/titleicon.svg b/openflexure_microscope/api/static/public/titleicon.svg new file mode 100644 index 00000000..f88a3088 --- /dev/null +++ b/openflexure_microscope/api/static/public/titleicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/openflexure_microscope/api/static/src/App.vue b/openflexure_microscope/api/static/src/App.vue new file mode 100644 index 00000000..9e1a31c1 --- /dev/null +++ b/openflexure_microscope/api/static/src/App.vue @@ -0,0 +1,470 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/assets/less/app.less b/openflexure_microscope/api/static/src/assets/less/app.less new file mode 100644 index 00000000..f762eda0 --- /dev/null +++ b/openflexure_microscope/api/static/src/assets/less/app.less @@ -0,0 +1,18 @@ +// Theming specific to the electron app + +.menubar-menu-container { + box-shadow: 0.5px 1px 4px 0px rgba(0, 0, 0, 0.3) !important; +} + +.menubar-menu-container>* { + box-shadow: none !important; +} + +.menubar-menu-container .action-menu-item, +.menubar-menu-container .action-label { + cursor: default +} + +.menubar-menu-container a:hover { + text-decoration: none; +} \ No newline at end of file diff --git a/openflexure_microscope/api/static/src/assets/less/highlight.less b/openflexure_microscope/api/static/src/assets/less/highlight.less new file mode 100644 index 00000000..0dc5923d --- /dev/null +++ b/openflexure_microscope/api/static/src/assets/less/highlight.less @@ -0,0 +1,67 @@ +/* + * UIkit Theme for Highlight.js + */ + +.hljs { + display: block; + color: #333; +} + +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: @global-danger-background; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #1f34aa; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} \ No newline at end of file diff --git a/openflexure_microscope/api/static/src/assets/less/theme.less b/openflexure_microscope/api/static/src/assets/less/theme.less new file mode 100644 index 00000000..7ab97249 --- /dev/null +++ b/openflexure_microscope/api/static/src/assets/less/theme.less @@ -0,0 +1,374 @@ +// UIkit Core +@import "../../../node_modules/uikit/src/less/uikit.theme.less"; + +// Highlight.js +@import "./highlight.less"; + +// Electron app theming +@import "./app.less"; + +// Custom OpenFlexure theming + +// +// Colors +// + +@global-color: #666; +@global-emphasis-color: #333; +@global-muted-color: #666; + +@global-primary-background: #C32280; + +@inverse-primary-muted-color: lighten(@global-primary-background, 15%); +@inverse-global-color: fade(@global-inverse-color, 80%); + +@global-border: #d5d5d5; + +// UIkit +// ======================================================================== + +@global-font-family: ProximaNova, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +@global-font-size: 14px; + +@global-xxlarge-font-size: 38px; +@global-xlarge-font-size: 30px; +@global-large-font-size: 24px; +@global-medium-font-size: 20px; +@global-small-font-size: 14px; + +@global-emphasis-color: #222; + +// +// Base +// + +@base-code-font-family: 'Roboto Mono', monospace; +@base-code-font-size: 12px; + +@base-heading-font-weight: 300; + +@base-pre-font-size: 12px; +@base-pre-padding: 25px; +@base-pre-background: @global-muted-background; +@base-pre-border-width: 0; +@base-pre-border-radius: 0; + +.hook-base-body() { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +// +// Decorations +// +@small-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); +@big-shadow: 0 14px 25px rgba(0, 0, 0, 0.16); + +// +// Container +// + +@container-max-width: 1380px; +@container-small-max-width: 650px; + +// +// Navbar +// + +@inverse-navbar-nav-item-color: @inverse-global-color; +@inverse-navbar-nav-item-hover-color: @inverse-global-emphasis-color; + +// +// Nav +// + +@nav-header-font-size: 12px; + +// +// Subnav +// + +@inverse-subnav-item-color: @inverse-global-color; +@inverse-subnav-item-hover-color: @inverse-global-emphasis-color; + +// +// Tab +// + +@tab-item-padding-horizontal: 10px; +@tab-item-padding-vertical: 9px; +@tab-item-border-width: 2px; +@tab-item-font-size: 12px; + +.hook-tab-item() { + line-height: 20px; +} + +// +// Table +// + +@table-header-cell-font-size: 12px; + +// +// Label +// + +@label-font-size: 12px; + +// +// Text +// + +.hook-text-lead() { + font-weight: 300; +} + +.hook-text-large() { + font-weight: 300; +} + +// +// Utility +// + +@inverse-logo-color: @inverse-global-emphasis-color; +@inverse-logo-hover-color: @inverse-global-emphasis-color; + +// +// Off-canvas +// + +@offcanvas-bar-background: #fff; +@offcanvas-bar-color-mode: dark; + +// +// Inverse +// + +@inverse-global-color: fade(@global-inverse-color, 80%); +@inverse-global-muted-color: fade(@global-inverse-color, 60%); + +// +// Paper +// +@paper-border-radius: 4px; +@button-border-radius: 3px; + + +/* ======================================================================== + Theme + ========================================================================== */ + +@sidebar-left-width: 240px; +@sidebar-left-width-xl: 300px; + +@sidebar-right-width: 200px; +@sidebar-right-left: 0px; +@sidebar-right-left-xl: 60px; + + + +/* UIkit modifiers + ========================================================================== */ + +/* + * Navbar + */ + +// Line Mode +@navbar-nav-item-line-margin-vertical: 20px; +@navbar-nav-item-line-margin-horizontal: @navbar-nav-item-padding-horizontal; +@navbar-nav-item-line-height: 1px; +@navbar-nav-item-line-background: currentColor; +@navbar-nav-item-line-transition-duration: 0.3s; + +.uk-navbar-dropdown.uk-open { + border-radius: @paper-border-radius; +} + +/* + * Cards + */ + +.uk-card { + border-radius: @paper-border-radius; + //border: 1px solid rgba(180, 180, 180, 0.25); + box-shadow: @small-shadow; +} + +.uk-card-media-top img { + border-radius: @paper-border-radius @paper-border-radius 0 0; +} + +.uk-card-default .uk-card-footer { + border-top: 1px solid rgba(180, 180, 180, 0.25); +} + +.uk-card-primary .uk-card-footer { + border-top: 1px solid rgba(0, 0, 0, 0.075); +} + +.uk-card-default { + color: @global-color; +} + +.hook-inverse() { + + // Override background colour in dark mode + .uk-card-default { + background-color: #2a2a2a !important; + color: @inverse-global-color; + } + + // Lighten on hover to show depth in dark mode + .uk-card-default:hover { + transition: background-color @animation-fast-duration ease; + background-color: #333 !important; + } +} + +/* + * Modal + */ +.uk-modal-dialog { + border-radius: @paper-border-radius; + box-shadow: @big-shadow; + transition: 0.3s ease !important; +} + +.uk-modal-footer { + border-radius: 0 0 @paper-border-radius @paper-border-radius; +} + +.uk-modal-header { + border-radius: @paper-border-radius @paper-border-radius 0 0; +} + +/* + * Notifications + */ +.uk-notification-message { + border-radius: @paper-border-radius; + box-shadow: @big-shadow; +} + +.uk-notification-message-danger { + border: 1px solid @notification-message-danger-color +} + +.uk-notification-message-warning { + border: 1px solid @notification-message-warning-color +} + +.uk-notification-message-success { + border: 1px solid @notification-message-success-color +} + +/* + * Links + */ +.uk-link, +.uk-button-link { + color: @global-muted-color; +} + +.uk-link:hover, +.uk-button-link:hover, +a:hover { + color: @global-primary-background; +} + +.hook-inverse() { + + .uk-link:hover, + .uk-button-link:hover, + a:hover { + color: @inverse-primary-muted-color; + } +} + +/* + * Buttons + */ +.uk-button { + border-radius: @button-border-radius; + padding: 0 8px; +} + +.uk-button-default { + background-color: rgba(180, 180, 180, 0.10); + border-color: @global-border; +} + +.uk-button-primary { + background-color: rgba(180, 180, 180, 0.10); + border-color: @global-primary-background; + color: @button-text-color; +} + +.uk-button-danger { + background-color: transparent; + color: #f0506e; + border: 1px solid #f0506e; +} + +.hook-inverse() { + .uk-button-primary { + background-color: rgba(180, 180, 180, 0.15); + color: @inverse-primary-muted-color; + border-color: @inverse-primary-muted-color; + } + + .uk-button-primary:hover { + background-color: @inverse-primary-muted-color; + color: #fff; + } +} + +.uk-icon-button, +.uk-icon-button:hover { + text-decoration: none +} + +/* + * Accordion + */ + +.uk-accordion-title { + display: block; + font-size: @global-font-size; + line-height: 1.4; + color: #333; + font-weight: 500; +} + +.uk-accordion-title::before { + content: ""; + width: 1.4em; + height: 1.4em; + float: left; + margin: 0 2px 0 0; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 17l5-5-5-5v10z'/%3E%3Cpath fill='none' d='M0 24V0h24v24H0z'/%3E%3C/svg%3E"); + mask-position: 100% 50%; + background-image: none; + background-color: #333; +} + +.uk-open>.uk-accordion-title::before { + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); + mask-position: 100% 50%; + background-image: none; +} + +.uk-accordion-content { + margin-top: 10px; +} + +.hook-inverse() { + .uk-accordion-title::before { + background-color: #fff; + } + +} \ No newline at end of file diff --git a/openflexure_microscope/api/static/src/components/appContent.vue b/openflexure_microscope/api/static/src/components/appContent.vue new file mode 100644 index 00000000..24439529 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/appContent.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/controlComponents/devTools.vue b/openflexure_microscope/api/static/src/components/controlComponents/devTools.vue new file mode 100644 index 00000000..7bcf540b --- /dev/null +++ b/openflexure_microscope/api/static/src/components/controlComponents/devTools.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/controlComponents/paneCapture.vue b/openflexure_microscope/api/static/src/components/controlComponents/paneCapture.vue new file mode 100644 index 00000000..41460dcc --- /dev/null +++ b/openflexure_microscope/api/static/src/components/controlComponents/paneCapture.vue @@ -0,0 +1,467 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/controlComponents/paneNavigate.vue b/openflexure_microscope/api/static/src/components/controlComponents/paneNavigate.vue new file mode 100644 index 00000000..fbaa567c --- /dev/null +++ b/openflexure_microscope/api/static/src/components/controlComponents/paneNavigate.vue @@ -0,0 +1,362 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/checkList.vue b/openflexure_microscope/api/static/src/components/fieldComponents/checkList.vue new file mode 100644 index 00000000..b39485d1 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/checkList.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/htmlBlock.vue b/openflexure_microscope/api/static/src/components/fieldComponents/htmlBlock.vue new file mode 100644 index 00000000..812c526f --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/htmlBlock.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/keyvalList.vue b/openflexure_microscope/api/static/src/components/fieldComponents/keyvalList.vue new file mode 100644 index 00000000..ed884a4a --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/keyvalList.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/labelInput.vue b/openflexure_microscope/api/static/src/components/fieldComponents/labelInput.vue new file mode 100644 index 00000000..5fc4a553 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/labelInput.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/numberInput.vue b/openflexure_microscope/api/static/src/components/fieldComponents/numberInput.vue new file mode 100644 index 00000000..4570308a --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/numberInput.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/radioList.vue b/openflexure_microscope/api/static/src/components/fieldComponents/radioList.vue new file mode 100644 index 00000000..361f73ff --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/radioList.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/selectList.vue b/openflexure_microscope/api/static/src/components/fieldComponents/selectList.vue new file mode 100644 index 00000000..d082b8eb --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/selectList.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/tagList.vue b/openflexure_microscope/api/static/src/components/fieldComponents/tagList.vue new file mode 100644 index 00000000..28aa065e --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/tagList.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/fieldComponents/textInput.vue b/openflexure_microscope/api/static/src/components/fieldComponents/textInput.vue new file mode 100644 index 00000000..d3f32e9b --- /dev/null +++ b/openflexure_microscope/api/static/src/components/fieldComponents/textInput.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/genericComponents/progressBar.vue b/openflexure_microscope/api/static/src/components/genericComponents/progressBar.vue new file mode 100644 index 00000000..783e2816 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/genericComponents/progressBar.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/genericComponents/tabContent.vue b/openflexure_microscope/api/static/src/components/genericComponents/tabContent.vue new file mode 100644 index 00000000..c3ef1ce4 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/genericComponents/tabContent.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/genericComponents/tabIcon.vue b/openflexure_microscope/api/static/src/components/genericComponents/tabIcon.vue new file mode 100644 index 00000000..e352f334 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/genericComponents/tabIcon.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/genericComponents/taskProgress.vue b/openflexure_microscope/api/static/src/components/genericComponents/taskProgress.vue new file mode 100644 index 00000000..1f355d0f --- /dev/null +++ b/openflexure_microscope/api/static/src/components/genericComponents/taskProgress.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/genericComponents/taskSubmitter.vue b/openflexure_microscope/api/static/src/components/genericComponents/taskSubmitter.vue new file mode 100644 index 00000000..488f0ed8 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/genericComponents/taskSubmitter.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/loadingContent.vue b/openflexure_microscope/api/static/src/components/loadingContent.vue new file mode 100644 index 00000000..5bd92b3a --- /dev/null +++ b/openflexure_microscope/api/static/src/components/loadingContent.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/modalComponents/calibrationModal.vue b/openflexure_microscope/api/static/src/components/modalComponents/calibrationModal.vue new file mode 100644 index 00000000..5383eca4 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/modalComponents/calibrationModal.vue @@ -0,0 +1,318 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/pluginComponents/JsonForm.vue b/openflexure_microscope/api/static/src/components/pluginComponents/JsonForm.vue new file mode 100644 index 00000000..70262bd8 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/pluginComponents/JsonForm.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/pluginComponents/WebComponentLoader.vue b/openflexure_microscope/api/static/src/components/pluginComponents/WebComponentLoader.vue new file mode 100644 index 00000000..810a73af --- /dev/null +++ b/openflexure_microscope/api/static/src/components/pluginComponents/WebComponentLoader.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/aboutContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/aboutContent.vue new file mode 100644 index 00000000..3f5ebd6e --- /dev/null +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/aboutContent.vue @@ -0,0 +1,35 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/captureContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/captureContent.vue new file mode 100644 index 00000000..a2959bd2 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/captureContent.vue @@ -0,0 +1,25 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/extensionContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/extensionContent.vue new file mode 100644 index 00000000..001d2e79 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/extensionContent.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue new file mode 100644 index 00000000..c7454cff --- /dev/null +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/galleryContent.vue @@ -0,0 +1,19 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/navigateContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/navigateContent.vue new file mode 100644 index 00000000..f29d4c5f --- /dev/null +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/navigateContent.vue @@ -0,0 +1,25 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/settingsContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/settingsContent.vue new file mode 100644 index 00000000..d9ad4810 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/settingsContent.vue @@ -0,0 +1,20 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/tabContentComponents/viewContent.vue b/openflexure_microscope/api/static/src/components/tabContentComponents/viewContent.vue new file mode 100644 index 00000000..0eebd4ec --- /dev/null +++ b/openflexure_microscope/api/static/src/components/tabContentComponents/viewContent.vue @@ -0,0 +1,20 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/connectComponents/hostCard.vue b/openflexure_microscope/api/static/src/components/viewComponents/connectComponents/hostCard.vue new file mode 100644 index 00000000..ee5ea2d6 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/connectComponents/hostCard.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/captureCard.vue b/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/captureCard.vue new file mode 100644 index 00000000..be3ccfdb --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/captureCard.vue @@ -0,0 +1,329 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/scanCard.vue b/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/scanCard.vue new file mode 100644 index 00000000..7432709f --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/scanCard.vue @@ -0,0 +1,149 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/zipDownloader.vue b/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/zipDownloader.vue new file mode 100644 index 00000000..c3eb4fbc --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/galleryComponents/zipDownloader.vue @@ -0,0 +1,184 @@ + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/galleryDisplay.vue b/openflexure_microscope/api/static/src/components/viewComponents/galleryDisplay.vue new file mode 100644 index 00000000..c33407d8 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/galleryDisplay.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/miniStreamDisplay.vue b/openflexure_microscope/api/static/src/components/viewComponents/miniStreamDisplay.vue new file mode 100644 index 00000000..e12f2e6d --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/miniStreamDisplay.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/appSettings.vue b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/appSettings.vue new file mode 100644 index 00000000..b47e42ee --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/appSettings.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraCalibrationSettings.vue b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraCalibrationSettings.vue new file mode 100644 index 00000000..06863da0 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraCalibrationSettings.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraSettings.vue b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraSettings.vue new file mode 100644 index 00000000..a7616481 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraSettings.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraStageMappingSettings.vue b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraStageMappingSettings.vue new file mode 100644 index 00000000..26dbf50e --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/cameraStageMappingSettings.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/microscopeSettings.vue b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/microscopeSettings.vue new file mode 100644 index 00000000..4a1c77a4 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/microscopeSettings.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/streamSettings.vue b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/streamSettings.vue new file mode 100644 index 00000000..5e5ae94e --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/settingsComponents/streamSettings.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/settingsDisplay.vue b/openflexure_microscope/api/static/src/components/viewComponents/settingsDisplay.vue new file mode 100644 index 00000000..9d3266dd --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/settingsDisplay.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/statusPane.vue b/openflexure_microscope/api/static/src/components/viewComponents/statusPane.vue new file mode 100644 index 00000000..4b970686 --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/statusPane.vue @@ -0,0 +1,202 @@ + + + + + + diff --git a/openflexure_microscope/api/static/src/components/viewComponents/streamDisplay.vue b/openflexure_microscope/api/static/src/components/viewComponents/streamDisplay.vue new file mode 100644 index 00000000..8b81bbce --- /dev/null +++ b/openflexure_microscope/api/static/src/components/viewComponents/streamDisplay.vue @@ -0,0 +1,333 @@ + + + + + diff --git a/openflexure_microscope/api/static/src/main.js b/openflexure_microscope/api/static/src/main.js new file mode 100644 index 00000000..b53b43a4 --- /dev/null +++ b/openflexure_microscope/api/static/src/main.js @@ -0,0 +1,158 @@ +import Vue from "vue"; +import App from "./App.vue"; +import store from "./store"; +import UIkit from "uikit"; +import VueTour from "vue-tour"; +import LoadScript from "vue-plugin-load-script"; +import VueFriendlyIframe from "vue-friendly-iframe"; + +require("vue-tour/dist/vue-tour.css"); + +// Import MD icons +import "material-design-icons/iconfont/material-icons.css"; + +// UIKit overrides +UIkit.mixin( + { + data: { + animation: false + } + }, + "accordion" +); + +// Use load-script module +Vue.use(LoadScript); + +// Use vue-tour module +Vue.use(VueTour); + +// Use Friendly Iframe module +Vue.use(VueFriendlyIframe); + +Vue.config.productionTip = false; + +Vue.mixin({ + methods: { + modalConfirm: function(modalText) { + var context = this; + + // Stop GPU preview to show modal + context.$root.$emit("globalTogglePreview", false); + + var showModal = function(resolve, reject) { + UIkit.modal + .confirm(modalText, { stack: true }) + .then( + function() { + resolve(); + }, + function() { + reject(); + } + ) + .finally(function() { + // Reenable the GPU preview, if it was active before the modal + console.log("Re-enabling GPU preview"); + if (context.$store.state.globalSettings.autoGpuPreview) { + console.log("Re-enabling preview"); + context.$root.$emit("globalTogglePreview", true); + } + }); + }; + return new Promise(showModal); + }, + + modalNotify: function(message, status = "success") { + UIkit.notification({ + message: message, + status: status + }); + }, + + modalDialog: function(title, message) { + UIkit.modal.dialog( + ` + +
+

${title}

+
+
+

${message}

+
+ `, + { stack: true } + ); + }, + + modalError: function(error) { + var errormsg = this.getErrorMessage(error); + this.$store.commit("setErrorMessage", errormsg); + UIkit.notification({ + message: `${errormsg}`, + status: "danger" + }); + }, + + getErrorMessage: function(error) { + var errormsg = ""; + + // If a response was obtained + if (error.response) { + // If the response is a nicely formatted JSON response from the server + if (error.response.data.message) { + errormsg = `${error.response.status}: ${error.response.data.message}`; + console.log(errormsg); + } + // If the response is just some generic error response + else { + errormsg = `${error.response.status}: ${error.response.data}`; + console.log(errormsg); + } + // If the error occured during the request + } else if (error.request) { + errormsg = `${error.message}`; + console.log(errormsg); + // Everything else + } else { + errormsg = `${error.message}`; + console.log(errormsg); + } + return errormsg; + }, + + showModalElement: function(element) { + UIkit.modal(element).show(); + }, + + hideModalElement: function(element) { + UIkit.modal(element).hide(); + }, + + toggleModalElement: function(element) { + UIkit.modal(element).toggle(); + }, + + getLocalStorageObj: function(keyName) { + if (localStorage.getItem(keyName)) { + try { + return JSON.parse(localStorage.getItem(keyName)); + } catch (e) { + console.log("Malformed entry. Removing from localStorage"); + localStorage.removeItem(keyName); + return null; + } + } + }, + + setLocalStorageObj: function(keyName, object) { + const parsed = JSON.stringify(object); + localStorage.setItem(keyName, parsed); + } + } +}); + +new Vue({ + store, + render: h => h(App) +}).$mount("#app"); diff --git a/openflexure_microscope/api/static/src/store.js b/openflexure_microscope/api/static/src/store.js new file mode 100644 index 00000000..00a4de76 --- /dev/null +++ b/openflexure_microscope/api/static/src/store.js @@ -0,0 +1,58 @@ +import Vue from "vue"; +import Vuex from "vuex"; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: { + origin: window.location.origin, + available: false, + waiting: false, + error: "", + globalSettings: { + disableStream: false, + autoGpuPreview: false, + trackWindow: true, + appTheme: "system" + }, + activeStreams: {} + }, + + mutations: { + changeOrigin(state, origin) { + state.origin = origin; + }, + changeWaiting(state, waiting) { + state.waiting = waiting; + }, + changeSetting(state, [key, value]) { + state.globalSettings[key] = value; + }, + resetState(state) { + state.waiting = false; + state.available = false; + state.error = null; + }, + setConnected(state) { + state.waiting = false; + state.available = true; + }, + setErrorMessage(state, msg) { + state.error = msg; + }, + addStream(state, id) { + state.activeStreams[id] = true; + }, + removeStream(state, id) { + state.activeStreams[id] = false; + } + }, + + actions: {}, + + getters: { + uriV2: state => `${state.origin}/api/v2`, + baseUri: state => state.origin, + ready: state => state.available + } +}); diff --git a/openflexure_microscope/api/static/vue.config.js b/openflexure_microscope/api/static/vue.config.js new file mode 100644 index 00000000..2983e5ab --- /dev/null +++ b/openflexure_microscope/api/static/vue.config.js @@ -0,0 +1,15 @@ +var webpack = require("webpack"); + +module.exports = { + configureWebpack: { + plugins: [ + new webpack.DefinePlugin({ + "process.env": { + PACKAGE: JSON.stringify(require("./package.json")) + } + }) + ] + }, + + publicPath: "" +}; diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities/__init__.py similarity index 51% rename from openflexure_microscope/api/utilities.py rename to openflexure_microscope/api/utilities/__init__.py index cdeca9d0..4b5a7cee 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities/__init__.py @@ -1,16 +1,38 @@ import logging +import os +import errno from werkzeug.exceptions import BadRequest -from flask import url_for +from flask import url_for, Blueprint, current_app + +from . import gui -class JsonPayload: +def view_class_from_endpoint(endpoint: str): + return current_app.view_functions[endpoint].view_class + + +def blueprint_for_module(module_name, api_ver=2, suffix=""): + return Blueprint( + blueprint_name_for_module(module_name, api_ver=api_ver, suffix=suffix), + module_name, + ) + + +def blueprint_name_for_module(module_name, api_ver=2, suffix=""): + bp_name = module_name.split(".")[-1] + return f"v{api_ver}_{bp_name}_blueprint{suffix}" + + +class JsonResponse: def __init__(self, request): """ Object to wrap up simple functionality for parsing a JSON response. """ # Try to load as json try: - self.json = request.get_json() #: dict: Dictionary representation of request JSON + self.json = ( + request.get_json() + ) #: dict: Dictionary representation of request JSON # If malformed JSON is passed, make an empty dictionary except BadRequest as e: logging.error(e) @@ -55,15 +77,12 @@ def gen(camera): # the obtained frame is a jpeg frame = camera.get_frame() - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") def get_bool(get_arg): """Convert GET request argument string to a Python bool""" - if (get_arg == 'true' or - get_arg == 'True' or - get_arg == '1'): + if get_arg == "true" or get_arg == "True" or get_arg == "1": return True else: return False @@ -80,10 +99,36 @@ def list_routes(app): endpoint = rule.endpoint methods = list(rule.methods) url = url_for(rule.endpoint, **options) - line = { - 'endpoint': endpoint, - 'methods': methods - } + line = {"endpoint": endpoint, "methods": methods} output[url] = line - - return output \ No newline at end of file + + return output + + +def create_file(config_path): + if not os.path.exists(os.path.dirname(config_path)): + try: + os.makedirs(os.path.dirname(config_path)) + except OSError as exc: # Guard against race condition + if exc.errno != errno.EEXIST: + raise + + +def init_default_extensions(extension_dir): + global _DEFAULT_EXTENSION_INIT + os.makedirs(extension_dir, exist_ok=True) + + default_ext_path = os.path.join(extension_dir, "defaults.py") + + if not os.path.isfile(default_ext_path): # If user extensions file doesn't exist + logging.warning( + "No extension file found at {}. Creating...".format(extension_dir) + ) + create_file(default_ext_path) + + logging.info("Populating {}...".format(default_ext_path)) + with open(default_ext_path, "w") as outfile: + outfile.write(_DEFAULT_EXTENSION_INIT) + + +_DEFAULT_EXTENSION_INIT = "from openflexure_microscope.api.default_extensions import *" diff --git a/openflexure_microscope/api/utilities/gui.py b/openflexure_microscope/api/utilities/gui.py new file mode 100644 index 00000000..6e00b04c --- /dev/null +++ b/openflexure_microscope/api/utilities/gui.py @@ -0,0 +1,61 @@ +import copy +import logging +from functools import wraps + + +def clean_rule(rule: str): + while rule[0] == "/": + rule = rule[1:] + return f"{rule}" + + +def build_gui_from_dict(gui_description, extension_object): + # Make a working copy of GUI description + api_gui = copy.deepcopy(gui_description) + + logging.debug(extension_object) + logging.debug(extension_object._rules) + + # Expand shorthand routes into full relative URLs + if "forms" in gui_description and isinstance(api_gui["forms"], list): + for form in api_gui["forms"]: + # Clean leading slashes from rule + if "route" in form: + form["route"] = clean_rule(form["route"]) + # Match rule in extension object + if "route" in form and form["route"] in extension_object._rules.keys(): + form["route"] = extension_object._rules[form["route"]]["urls"][0] + else: + logging.warn( + "No valid expandable route found for {}".format(form["route"]) + ) + + # Inject extension information + api_gui["id"] = extension_object.name + api_gui["version"] = extension_object.version + return api_gui + + +def build_gui_from_func(func, extension_object): + @wraps(func) + def wrapped(*args, **kwargs): + return build_gui_from_dict(func(*args, **kwargs), extension_object) + + return wrapped + + +def build_gui(gui_description, extension_object): + # If given a function that generates a GUI dictionary + if callable(gui_description): + # Wrap in the route expander + return build_gui_from_func(gui_description, extension_object) + # If given a dictionary directly + elif isinstance(gui_description, dict): + # Build a GUI generator function + def gui_description_func(): + return gui_description + + # Wrap in the route expander + return build_gui_from_func(gui_description_func, extension_object) + else: + raise RuntimeError("GUI description must be a function or a dictionary") diff --git a/openflexure_microscope/api/v1/blueprints/__init__.py b/openflexure_microscope/api/v1/blueprints/__init__.py deleted file mode 100644 index eea7c36c..00000000 --- a/openflexure_microscope/api/v1/blueprints/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import camera, stage, base, plugins, task diff --git a/openflexure_microscope/api/v1/blueprints/base.py b/openflexure_microscope/api/v1/blueprints/base.py deleted file mode 100644 index 3ef923ca..00000000 --- a/openflexure_microscope/api/v1/blueprints/base.py +++ /dev/null @@ -1,236 +0,0 @@ -from openflexure_microscope.api.utilities import gen, JsonPayload -from openflexure_microscope.api.v1.views import MicroscopeView - -from flask import Response, Blueprint, jsonify, request -import logging - - -class StreamAPI(MicroscopeView): - - def get(self): - """ - Real-time MJPEG stream from the microscope camera - - .. :quickref: State; Camera stream - - :>header Accept: image/jpeg - :>header Content-Type: image/jpeg - :status 200: stream active - """ - # Restart stream worker thread - self.microscope.camera.start_worker() - - return Response( - gen(self.microscope.camera), - mimetype='multipart/x-mixed-replace; boundary=frame') - - -class StateAPI(MicroscopeView): - - def get(self): - """ - JSON representation of the microscope object. - - .. :quickref: State; Microscope state - - **Example request**: - - .. sourcecode:: http - - GET /state/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "camera": { - "preview_active": false, - "record_active": false, - "stream_active": true - }, - "plugin": {}, - "stage": { - "backlash": { - "x": 128, - "y": 128, - "z": 128 - }, - "position": { - "x": -8080, - "y": 5665, - "z": -12600 - } - } - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.state) - - -class ConfigAPI(MicroscopeView): - - def get(self): - """ - JSON representation of the microscope config. - - .. :quickref: Config; Get microscope config - - **Example request**: - - .. sourcecode:: http - - GET /config/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.read_config(json_safe=True)) - - def post(self): - """ - Modify microscope configuration - - .. :quickref: Config; Set microscope config - - **Example request**: - - .. sourcecode:: http - - POST /config HTTP/1.1 - Accept: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - - :/metadata/', - view_func=capture.MetadataAPI.as_view('metadata_download', microscope=microscope_obj)) - - blueprint.add_url_rule( - '/capture//metadata', - view_func=capture.MetadataRedirectAPI.as_view('metadata_download_redirect', microscope=microscope_obj)) - - # Tag routes - blueprint.add_url_rule( - '/capture//tags', - view_func=capture.TagsAPI.as_view('capture_tags', microscope=microscope_obj)) - - # Capture routes - blueprint.add_url_rule( - '/capture//download/', - view_func=capture.DownloadAPI.as_view('capture_download', microscope=microscope_obj)) - - blueprint.add_url_rule( - '/capture//download', - view_func=capture.DownloadRedirectAPI.as_view('capture_download_redirect', microscope=microscope_obj)) - - blueprint.add_url_rule( - '/capture//', - view_func=capture.CaptureAPI.as_view('capture', microscope=microscope_obj)) - - blueprint.add_url_rule( - '/capture/', - view_func=capture.ListAPI.as_view('capture_list', microscope=microscope_obj)) - - # Preview routes - blueprint.add_url_rule( - '/preview/', - view_func=preview.GPUPreviewAPI.as_view('gpu_preview', microscope=microscope_obj)) - - # Function routes - blueprint.add_url_rule( - '/overlay', - view_func=function.OverlayAPI.as_view('overlay', microscope=microscope_obj)) - - blueprint.add_url_rule( - '/zoom', - view_func=function.ZoomAPI.as_view('zoom', microscope=microscope_obj)) - - return(blueprint) \ No newline at end of file diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py deleted file mode 100644 index 3af8ee09..00000000 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ /dev/null @@ -1,512 +0,0 @@ -from openflexure_microscope.api.utilities import get_bool, JsonPayload -from openflexure_microscope.api.v1.views import MicroscopeView -from openflexure_microscope.utilities import filter_dict - -from flask import Response, jsonify, request, abort, url_for, redirect, send_file - - -class ListAPI(MicroscopeView): - - def get(self): - """ - Get list of image captures. - - .. :quickref: Captures; Get collection of captures - - :>header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>jsonarr boolean available: availability of capture data - :>jsonarr string filename: filename of capture - :>jsonarr string id: unique id of the capture object - :>jsonarr boolean keep_on_disk: keep the capture file on microscope after closing - :>jsonarr boolean locked: file locked for modifications (mostly used for video recording) - :>jsonarr string path: path on pi storage to the capture file, if available - :>jsonarr boolean stream: capture stored in-memory as a BytesIO stream - :>jsonarr json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :>header Content-Type: application/json - :status 200: capture found - :status 404: no capture found with that id - """ - include_unavailable = get_bool(request.args.get('include_unavailable')) - - if include_unavailable: - captures = [image.state for image in self.microscope.camera.images] - else: - captures = [image.state for image in self.microscope.camera.images if image.state['available']] - - return jsonify(captures) - - def delete(self): - """ - Delete all captures (not yet implemented) - - .. :quickref: Captures; Delete all captures - """ - for image in self.microscope.camera.images: - image.delete() - - captures = [image.state for image in self.microscope.camera.images] - - return jsonify(captures) - - def post(self): - """ - Create a new image capture. - - .. :quickref: Captures; New capture - - **Example request**: - - .. sourcecode:: http - - POST /camera/capture HTTP/1.1 - Accept: application/json - - { - "filename": "myfirstcapture", - "temporary": false, - "use_video_port": true, - "bayer": true, - "size": { - "width": 640, - "height": 480 - } - } - - :>header Accept: application/json - - :json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean temporary: delete the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :
json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean keep_on_disk: keep the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj: - return abort(404) # 404 Not Found - - # Get capture state - capture_metadata = capture_obj.state - - # Add API routes to returned state - uri_dict = { - 'uri': {'state': '{}'.format(url_for('.capture', capture_id=capture_obj.id))} - } - - # If available, also add download link - if capture_metadata['available']: - uri_dict['uri']['download'] = '{}download/{}'.format( - url_for('.capture', capture_id=capture_obj.id), - capture_obj.filename - ) - - capture_metadata.update(uri_dict) - - return jsonify(capture_metadata) - - def delete(self, capture_id): - """ - Delete all capture data from the Pi, even if `keep_on_disk=true;`. - - .. :quickref: Capture; Delete capture. - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj: - return abort(404) # 404 Not Found - - capture_obj.delete() - - return jsonify({"return": capture_id}) - - def put(self, capture_id): - """ - Add arbitrary metadata to the capture (stored in an accompanying capture `.yaml` file) - - .. :quickref: Capture; Update capture metadata - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b HTTP/1.1 - Accept: application/json - - { - "user_id": "ofm_1234", - "patient_number": 1452, - } - - :>header Accept: application/json - - :
header Accept: image/jpeg - :query thumbnail: return an image thumbnail e.g. ?thumbnail=true - - :>header Content-Type: image/jpeg - :status 200: capture data found - :status 404: no capture found with that id - - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state['available']: - return abort(404) # 404 Not Found - - thumbnail = get_bool(request.args.get('thumbnail')) - - return redirect(url_for( - '.capture_download', - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail - ), code=307) - - -class DownloadAPI(MicroscopeView): - def get(self, capture_id, filename): - - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state['available']: - return abort(404) # 404 Not Found - - thumbnail = get_bool(request.args.get('thumbnail')) - - # If no filename is specified, redirect to the capture's currently set filename - if not filename: - return redirect(url_for( - 'capture_download', - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail - ), code=307) - - # Download the image data using the requested filename - if thumbnail: - img = capture_obj.thumbnail - else: - img = capture_obj.data - - return send_file( - img, - mimetype='image/jpeg') - - -class MetadataRedirectAPI(MicroscopeView): - def get(self, capture_id): - """ - Return metadatadata for a capture. - - Return capture metadata as a YAML file with the requested filename. - I.e., `/(capture_id)/download/bar.yaml` will download the file as - `bar.yaml`, regardless of the capture's initially set filename. - - Route automatically redirects to download the capture metadata under it's currently set filename. - I.e., `/(capture_id)/download` will - redirect to `/(capture_id)/download/(filename)`. - - .. :quickref: Capture; Download capture metadata - - **Example request**: - - .. sourcecode:: http - - GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/metadata/2018-11-20_16-04-17.yaml HTTP/1.1 - Accept: text/yaml - - :>header Accept: text/yaml - :query as_attachment: return the image as an attachment download e.g. ?as_attachment=true - - :>header Content-Type: text/yaml - :status 200: capture data found - :status 404: no capture found with that id - - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state['available']: - return abort(404) # 404 Not Found - - return redirect(url_for( - '.metadata_download', - capture_id=capture_id, - filename=capture_obj.metadataname - ), code=307) - - -class MetadataAPI(MicroscopeView): - def get(self, capture_id, filename): - - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state['available']: - return abort(404) # 404 Not Found - - # If no filename is specified, redirect to the capture's currently set filename - if not filename: - return redirect(url_for( - 'capture_download', - capture_id=capture_id, - filename=capture_obj.metadataname - ), code=307) - - # Download the metadata using the requested filename - data = capture_obj.yaml - - return Response( - data, - mimetype="text/yaml") - - -class TagsAPI(MicroscopeView): - def get(self, capture_id): - """ - Return tag list for a capture. - - .. :quickref: Capture; Show capture tags - - **Example request**: - - .. sourcecode:: http - - GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: text/yaml - - :>header Accept: text/yaml - - :>header Content-Type: text/yaml - :status 200: capture data found - :status 404: no capture found with that id - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state['available']: - return abort(404) # 404 Not Found - - metadata_tags = filter_dict(capture_obj.state, ['metadata', 'tags']) - - return jsonify(metadata_tags) - - def put(self, capture_id): - """ - Add tags to the capture - - .. :quickref: Capture; Update capture tags - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: application/json - - ["tests", "mytag", "someothertag"] - - :>header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>header Content-Type: application/json - """ - - data = self.microscope.task.state - - return jsonify(data) - - def delete(self): - """ - Clean list of long-running tasks (running tasks persist). - - .. :quickref: Tasks; Clean collection of tasks - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - self.microscope.task.clean() - data = self.microscope.task.state - - return jsonify(data) - - -class TaskAPI(MicroscopeView): - - def get(self, task_id): - """ - Get JSON representation of a task - - .. :quickref: Tasks; Get task - - **Example request**: - - .. sourcecode:: http - - GET /task/db13a66787e1419bb06b1504e4d80b0c/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "end_time": "2019-01-23 16-33-33", - "id": "db13a66787e1419bb06b1504e4d80b0c", - "return": [ - 0.848622546386467, - 0.6106785018091292, - ], - "start_time": "2019-01-23 16-33-13", - "status": "success" - } - - """ - - task = self.microscope.task.task_from_id(task_id) - - if not task_id: - return abort(404) # 404 Not Found - - # Return task state - return jsonify(task.state) - - def delete(self, task_id): - """ - Remove a particular task (fails if task is currently running). - - .. :quickref: Tasks; Remove a task - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - success = self.microscope.task.delete(task_id) - - if success: - data = { - 'status': 'success', - 'message': 'task successfully deleted' - } - else: - data = { - 'status': 'fail', - 'message': 'task could not be deleted, as it is currently running or does not exist' - } - - return jsonify(data) - - -def construct_blueprint(microscope_obj): - - blueprint = Blueprint('task_blueprint', __name__) - - blueprint.add_url_rule( - '/', - view_func=TaskListAPI.as_view('task_list', microscope=microscope_obj) - ) - - blueprint.add_url_rule( - '//', - view_func=TaskAPI.as_view('task', microscope=microscope_obj) - ) - - return blueprint diff --git a/openflexure_microscope/api/v1/views.py b/openflexure_microscope/api/v1/views.py deleted file mode 100644 index 3c32ad44..00000000 --- a/openflexure_microscope/api/v1/views.py +++ /dev/null @@ -1,26 +0,0 @@ -from flask.views import MethodView - - -class MicroscopeView(MethodView): - """ - Create a generic MethodView with a globally available - microscope object passed as an argument. - """ - def __init__(self, microscope, **kwargs): - - self.microscope = microscope - - MethodView.__init__(self, **kwargs) - - -class MicroscopeViewPlugin(MicroscopeView): - """ - Create a generic MethodView with a globally available - microscope object passed as an argument, and a plugin - reference stored in 'self'. Initially None. - """ - def __init__(self, microscope, plugin=None, **kwargs): - - self.plugin = plugin - - MicroscopeView.__init__(self, microscope=microscope, **kwargs) diff --git a/openflexure_microscope/plugins/default/__init__.py b/openflexure_microscope/api/v2/__init__.py similarity index 100% rename from openflexure_microscope/plugins/default/__init__.py rename to openflexure_microscope/api/v2/__init__.py diff --git a/openflexure_microscope/api/v2/views/__init__.py b/openflexure_microscope/api/v2/views/__init__.py new file mode 100644 index 00000000..3c1fae87 --- /dev/null +++ b/openflexure_microscope/api/v2/views/__init__.py @@ -0,0 +1,4 @@ +from .actions import enabled_root_actions +from .captures import * +from .instrument import * +from .streams import * diff --git a/openflexure_microscope/api/v2/views/actions/__init__.py b/openflexure_microscope/api/v2/views/actions/__init__.py new file mode 100644 index 00000000..1c26e31a --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/__init__.py @@ -0,0 +1,86 @@ +""" +Top-level representation of enabled actions +""" +from flask import url_for +from . import camera, stage, system + +from labthings import current_labthing +from labthings.views import View +from labthings.utilities import description_from_view + +_actions = { + "capture": { + "rule": "/camera/capture/", + "view_class": camera.CaptureAPI, + "conditions": True, + }, + "ramCapture": { + "rule": "/camera/ram-capture/", + "view_class": camera.RAMCaptureAPI, + "conditions": True, + }, + "previewStart": { + "rule": "/camera/preview/start", + "view_class": camera.GPUPreviewStartAPI, + "conditions": True, + }, + "previewStop": { + "rule": "/camera/preview/stop", + "view_class": camera.GPUPreviewStopAPI, + "conditions": True, + }, + "move": { + "rule": "/stage/move/", + "view_class": stage.MoveStageAPI, + "conditions": True, + }, + "zeroStage": { + "rule": "/stage/zero/", + "view_class": stage.ZeroStageAPI, + "conditions": True, + }, + "shutdown": { + "rule": "/system/shutdown/", + "view_class": system.ShutdownAPI, + "conditions": system.is_raspberrypi(), + }, + "reboot": { + "rule": "/system/reboot/", + "view_class": system.RebootAPI, + "conditions": system.is_raspberrypi(), + }, +} + + +def enabled_root_actions(): + global _actions + return {k: v for k, v in _actions.items() if v["conditions"]} + + +class ActionsView(View): + def get(self): + """ + List of enabled default API actions. + + This list does not include any actions added by LabThings extensions, only + those part of the default OpenFlexure Microscope API. + """ + global _actions + + actions = {} + for name, action in enabled_root_actions().items(): + d = { + "links": { + "self": { + "href": url_for(action["view_class"].endpoint, _external=True), + "mimetype": "application/json", + **description_from_view(action["view_class"]), + } + }, + "rule": action["rule"], + "view_class": str(action["view_class"]), + } + + actions[name] = d + + return actions diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py new file mode 100644 index 00000000..a2dc68e8 --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -0,0 +1,157 @@ +from openflexure_microscope.api.utilities import get_bool, JsonResponse +from labthings.views import View, ActionView +from labthings import find_component, fields + +from openflexure_microscope.utilities import filter_dict + +from openflexure_microscope.api.v2.views.captures import CaptureSchema + +import logging +import io +from flask import request, abort, url_for, redirect, send_file + + +class CaptureAPI(ActionView): + """ + Create a new image capture. + """ + + args = { + "filename": fields.String(example="MyFileName"), + "temporary": fields.Boolean( + missing=False, description="Delete capture on shutdown" + ), + "use_video_port": fields.Boolean(missing=False), + "bayer": fields.Boolean( + missing=False, description="Store raw bayer data in file" + ), + "annotations": fields.Dict(missing={}, example={"Client": "SwaggerUI"}), + "tags": fields.List(fields.String, missing=[], example=["docs"]), + "resize": fields.Dict( + missing=None, example={"width": 640, "height": 480} + ), # TODO: Validate keys + } + schema = CaptureSchema() + + def post(self, args): + """ + Create a new capture + """ + microscope = find_component("org.openflexure.microscope") + + resize = args.get("resize", None) + if resize: + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple + else: + abort(404) + + # Explicitally acquire lock (prevents empty files being created if lock is unavailable) + with microscope.camera.lock: + return microscope.capture( + filename=args.get("filename"), + temporary=args.get("temporary"), + use_video_port=args.get("use_video_port"), + resize=resize, + bayer=args.get("bayer"), + annotations=args.get("annotations"), + tags=args.get("tags") + ) + + + +class RAMCaptureAPI(ActionView): + """ + Take a non-persistant image capture. + """ + + args = { + "use_video_port": fields.Boolean(missing=True), + "bayer": fields.Boolean( + missing=False, description="Return with raw bayer data" + ), + "resize": fields.Dict( + missing=None, example={"width": 640, "height": 480} + ), # TODO: Validate keys + } + + responses = { + 200: { + "content_type": "image/jpeg" + } + } + + def post(self, args): + """ + Take a non-persistant image capture. + """ + microscope = find_component("org.openflexure.microscope") + + resize = args.get("resize", None) + if resize: + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple + else: + abort(404) + + # Open a BytesIO stream to be destroyed once request has returned + with microscope.camera.lock, io.BytesIO() as stream: + + microscope.camera.capture( + stream, + use_video_port=args.get("use_video_port"), + resize=resize, + bayer=args.get("bayer"), + ) + + stream.seek(0) + + return send_file(io.BytesIO(stream.getbuffer()), mimetype="image/jpeg") + + +class GPUPreviewStartAPI(ActionView): + """ + Start the onboard GPU preview. + Optional "window" parameter can be passed to control the position and size of the preview window, + in the format ``[x, y, width, height]``. + """ + + args = {"window": fields.List(fields.Integer, missing=[], example=[0, 0, 640, 480])} + + def post(self, args): + """ + Start the onboard GPU preview. + """ + microscope = find_component("org.openflexure.microscope") + + window = args.get("window") + logging.debug(window) + + if len(window) != 4: + fullscreen = True + window = None + else: + fullscreen = False + window = [int(w) for w in window] + + microscope.camera.start_preview(fullscreen=fullscreen, window=window) + + # TODO: Make schema for microscope state + return microscope.state + + +class GPUPreviewStopAPI(ActionView): + def post(self): + """ + Stop the onboard GPU preview. + """ + microscope = find_component("org.openflexure.microscope") + microscope.camera.stop_preview() + # TODO: Make schema for microscope state + return microscope.state diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py new file mode 100644 index 00000000..cb26823f --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/stage.py @@ -0,0 +1,67 @@ +from openflexure_microscope.api.utilities import JsonResponse +from labthings.views import View, ActionView +from labthings import find_component, fields + +from openflexure_microscope.utilities import axes_to_array, filter_dict + +from flask import Blueprint, request + +import logging + + +class MoveStageAPI(ActionView): + args = { + "absolute": fields.Boolean( + missing=False, example=False, description="Move to an absolute position" + ), + "x": fields.Int(missing=0, example=100), + "y": fields.Int(missing=0, example=100), + "z": fields.Int(missing=0, example=20), + } + + def post(self, args): + """ + Move the microscope stage in x, y, z + """ + microscope = find_component("org.openflexure.microscope") + + # Handle absolute positioning (calculate a relative move from current position and target) + if (args.get("absolute")) and (microscope.stage): # Only if stage exists + target_position = axes_to_array(args, ["x", "y", "z"]) + logging.debug("TARGET: {}".format(target_position)) + position = [ + target_position[i] - microscope.stage.position[i] for i in range(3) + ] + logging.debug("DELTA: {}".format(position)) + + else: + # Get coordinates from payload + position = axes_to_array(args, ["x", "y", "z"], [0, 0, 0]) + + logging.debug(position) + + # Move if stage exists + if microscope.stage: + # Explicitally acquire lock with 1s timeout + with microscope.stage.lock(timeout=1): + microscope.stage.move_rel(position) + else: + logging.warning("Unable to move. No stage found.") + + # TODO: Make schema for microscope state + return microscope.state["stage"]["position"] + + +class ZeroStageAPI(ActionView): + def post(self): + """ + Zero the stage coordinates. + Does not move the stage, but rather makes the current position read as [0, 0, 0] + """ + microscope = find_component("org.openflexure.microscope") + + with microscope.stage.lock(timeout=1): + microscope.stage.zero_position() + + # TODO: Make schema for microscope state + return microscope.state["stage"] diff --git a/openflexure_microscope/api/v2/views/actions/system.py b/openflexure_microscope/api/v2/views/actions/system.py new file mode 100644 index 00000000..328ff16f --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/system.py @@ -0,0 +1,50 @@ +from labthings.views import View, ActionView +import subprocess +import os +from sys import platform + + +def is_raspberrypi(raise_on_errors=False): + """ + Checks if Raspberry Pi. + """ + # I mean, if it works, it works... + return os.path.exists("/usr/bin/raspi-config") + + +class ShutdownAPI(ActionView): + """ + Attempt to shutdown the device + """ + + def post(self): + """ + Attempt to shutdown the device + """ + p = subprocess.Popen( + ["sudo", "shutdown", "-h", "now"], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + out, err = p.communicate() + return {"out": out, "err": err} + + +class RebootAPI(ActionView): + """ + Attempt to reboot the device + """ + + def post(self): + """ + Attempt to reboot the device + """ + p = subprocess.Popen( + ["sudo", "shutdown", "-r", "now"], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + + out, err = p.communicate() + return {"out": out, "err": err} diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py new file mode 100644 index 00000000..cd9b1e92 --- /dev/null +++ b/openflexure_microscope/api/v2/views/captures.py @@ -0,0 +1,261 @@ +import logging +from flask import abort, request, redirect, url_for, send_file + +from openflexure_microscope.api.utilities import get_bool, JsonResponse + +from labthings import Schema, fields, find_component +from labthings.views import View, PropertyView +from labthings.utilities import description_from_view +from labthings.views.marshalling import marshal_with + +from marshmallow import pre_dump + + +class InstrumentSchema(Schema): + id = fields.UUID() + configuration = fields.Dict() + settings = fields.Dict() + state = fields.Dict() + + +class CaptureMetadataImageSchema(Schema): + id = fields.UUID() + acquisitionDate = fields.String(format="date") + format = fields.String() + name = fields.String() + tags = fields.List(fields.String()) + annotations = fields.Dict() + + +class CaptureMetadataSchema(Schema): + experimenter = fields.Dict() # TODO: Make schema + experimenterGroup = fields.Dict() # TODO: Make schema + dataset = fields.Dict() # TODO: Make schema + image = fields.Nested(CaptureMetadataImageSchema()) + instrument = fields.Nested(InstrumentSchema()) + + +class CaptureSchema(Schema): + id = fields.String() + file = fields.String( + data_key="path", description="Path of file on microscope device" + ) + exists = fields.Bool(data_key="available") + name = fields.String() + metadata = fields.Nested(CaptureMetadataSchema()) + + links = fields.Dict() + + # TODO: Automate this somewhat + @pre_dump + def generate_links(self, data, **kwargs): + data.links = { + "self": { + "href": url_for(CaptureView.endpoint, id=data.id, _external=True), + "mimetype": "application/json", + **description_from_view(CaptureView), + }, + "tags": { + "href": url_for(CaptureTags.endpoint, id=data.id, _external=True), + "mimetype": "application/json", + **description_from_view(CaptureTags), + }, + "annotations": { + "href": url_for( + CaptureAnnotations.endpoint, id=data.id, _external=True + ), + "mimetype": "application/json", + **description_from_view(CaptureAnnotations), + }, + "download": { + "href": url_for( + CaptureDownload.endpoint, + id=data.id, + filename=data.name, + _external=True, + ), + "mimetype": "image/jpeg", + **description_from_view(CaptureDownload), + }, + } + return data + + +class CaptureList(PropertyView): + tags = ["captures"] + schema = CaptureSchema(many=True) + + def get(self): + """ + List all image captures + """ + microscope = find_component("org.openflexure.microscope") + image_list = microscope.captures.images.values() + return image_list + + +class CaptureView(View): + tags = ["captures"] + + @marshal_with(CaptureSchema()) + def get(self, id): + """ + Description of a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return capture_obj + + def delete(self, id): + """ + Delete a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + # Delete the capture file + capture_obj.delete() + # Delete from capture list + del microscope.captures.images[id] + + return "", 204 + + +class CaptureDownload(View): + tags = ["captures"] + responses = { + 200: { + "content_type": "image/jpeg" + } + } + + def get(self, id, filename): + """ + Image data for a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + thumbnail = get_bool(request.args.get("thumbnail")) + + # If no filename is specified, redirect to the capture's currently set filename + if not filename: + return redirect( + url_for( + "DownloadAPI", + id=id, + filename=capture_obj.filename, + thumbnail=thumbnail, + ), + code=307, + ) + + # Download the image data using the requested filename + if thumbnail: + img = capture_obj.thumbnail + else: + img = capture_obj.data + + return send_file(img, mimetype="image/jpeg") + + +class CaptureTags(View): + tags = ["captures"] + + def get(self, id): + """ + Get tags associated with a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return capture_obj.tags + + def put(self, id): + """ + Add tags to a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + # TODO: Replace with normal Flask request JSON thing + data_dict = JsonResponse(request).json + + if type(data_dict) != list: + return abort(400) + + capture_obj.put_tags(data_dict) + + return capture_obj.tags + + def delete(self, id): + """ + Delete tags from a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + data_dict = JsonResponse(request).json + + if type(data_dict) != list: + return abort(400) + + for tag in data_dict: + capture_obj.delete_tag(str(tag)) + + return capture_obj.tags + + +class CaptureAnnotations(View): + tags = ["captures"] + + def get(self, id): + """ + Get annotations associated with a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return capture_obj.annotations + + def put(self, id): + """ + Update metadata for a single image capture + """ + microscope = find_component("org.openflexure.microscope") + capture_obj = microscope.captures.images.get(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + data_dict = JsonResponse(request).json + logging.debug(data_dict) + + if type(data_dict) != dict: + return abort(400) + + capture_obj.put_annotations(data_dict) + + return capture_obj.annotations diff --git a/openflexure_microscope/api/v2/views/instrument.py b/openflexure_microscope/api/v2/views/instrument.py new file mode 100644 index 00000000..f6d5c73f --- /dev/null +++ b/openflexure_microscope/api/v2/views/instrument.py @@ -0,0 +1,129 @@ +from openflexure_microscope.api.utilities import JsonResponse + +from labthings import find_component +from labthings.views import View, PropertyView +from labthings.utilities import get_by_path, set_by_path, create_from_path + +from flask import request, abort +import logging + + +class SettingsProperty(PropertyView): + def get(self): + """ + Current microscope settings, including camera and stage + """ + microscope = find_component("org.openflexure.microscope") + return microscope.read_settings() + + def put(self): + """ + Update current microscope settings, including camera and stage + """ + microscope = find_component("org.openflexure.microscope") + payload = JsonResponse(request) + + logging.debug("Updating settings from PUT request:") + logging.debug(payload.json) + + microscope.update_settings(payload.json) + microscope.save_settings() + + return self.get() + + +class NestedSettingsProperty(View): + tags = ["properties"] + responses = { + 404: {"description": "Settings key cannot be found"} + } + + def get(self, route): + """ + Show a nested section of the current microscope settings + """ + microscope = find_component("org.openflexure.microscope") + keys = route.split("/") + + try: + value = get_by_path(microscope.read_settings(), keys) + except KeyError: + return abort(404) + + return value + + def put(self, route): + """ + Update a nested section of the current microscope settings + """ + microscope = find_component("org.openflexure.microscope") + keys = route.split("/") + payload = JsonResponse(request) + + dictionary = create_from_path(keys) + set_by_path(dictionary, keys, payload.json) + + microscope.update_settings(dictionary) + microscope.save_settings() + + return self.get(route) + + +class StateProperty(PropertyView): + def get(self): + """ + Show current read-only state of the microscope + """ + microscope = find_component("org.openflexure.microscope") + return microscope.state + + +class NestedStateProperty(View): + tags = ["properties"] + responses = { + 404: {"description": "Status key cannot be found"} + } + + def get(self, route): + """ + Show a nested section of the current microscope state + """ + microscope = find_component("org.openflexure.microscope") + keys = route.split("/") + + try: + value = get_by_path(microscope.state, keys) + except KeyError: + return abort(404) + + return value + + +class ConfigurationProperty(PropertyView): + def get(self): + """ + Show current read-only state of the microscope + """ + microscope = find_component("org.openflexure.microscope") + return microscope.configuration + + +class NestedConfigurationProperty(View): + tags = ["properties"] + responses = { + 404: {"description": "Status key cannot be found"} + } + + def get(self, route): + """ + Show a nested section of the current microscope state + """ + microscope = find_component("org.openflexure.microscope") + keys = route.split("/") + + try: + value = get_by_path(microscope.configuration, keys) + except KeyError: + return abort(404) + + return value diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py new file mode 100644 index 00000000..caca7eb8 --- /dev/null +++ b/openflexure_microscope/api/v2/views/streams.py @@ -0,0 +1,62 @@ +from openflexure_microscope.api.utilities import gen, JsonResponse + +from labthings import find_component +from labthings.utilities import get_by_path, set_by_path, create_from_path +from labthings.views import View, PropertyView + +from flask import Response + + +class MjpegStream(PropertyView): + """ + Real-time MJPEG stream from the microscope camera + """ + responses = { + 200: { + "content_type": "multipart/x-mixed-replace" + } + } + + def get(self): + """ + MJPEG stream from the microscope camera. + + Note: While the code actually getting frame data from a camera and storing it in + camera.frame runs in a thread, the gen(microscope.camera) generator does not. + This response is therefore blocking. The generator just yields the most recent + frame from the camera object, passed to the Flask response, and then repeats until + the connection is closed. + + Without monkey patching, or using a native threaded server, the stream + will block all proceeding requests. + """ + microscope = find_component("org.openflexure.microscope") + # Restart stream worker thread + microscope.camera.start_worker() + + return Response( + gen(microscope.camera), mimetype="multipart/x-mixed-replace; boundary=frame" + ) + + +class SnapshotStream(PropertyView): + """ + Single JPEG snapshot from the camera stream + """ + + responses = { + 200: { + "content_type": "image/jpeg", + "description": "Snapshot taken" + } + } + + def get(self): + """ + Single snapshot from the camera stream + """ + microscope = find_component("org.openflexure.microscope") + # Restart stream worker thread + microscope.camera.start_worker() + + return Response(microscope.camera.get_frame(), mimetype="image/jpeg") diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 1ea62010..8f4e62a4 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -1,152 +1,66 @@ # -*- coding: utf-8 -*- import time import os -import threading +import shutil import datetime import logging +import threading + from abc import ABCMeta, abstractmethod -try: - from greenlet import getcurrent as get_ident -except ImportError: - try: - from thread import get_ident - except ImportError: - from _thread import get_ident - -from .capture import CaptureObject, BASE_CAPTURE_PATH, TEMP_CAPTURE_PATH -from openflexure_microscope.utilities import entry_by_id -from openflexure_microscope.lock import StrictLock - - -def last_entry(object_list: list): - """Return the last entry of a list, if the list contains items.""" - if object_list: # If any images have been captured - return object_list[-1] # Return the latest captured image - else: - return None - - -def generate_basename(): - """Return a default filename based on the capture datetime""" - return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - - -def generate_numbered_basename(obj_list: list) -> str: - initial_basename = generate_basename() - basename = initial_basename - # Handle clashing - iterator = 1 - while basename in [obj.basename for obj in obj_list]: - basename = initial_basename + "_{}".format(iterator) - iterator += 1 - - return basename - - -def shunt_captures(target_list: list): - for obj in target_list: # For each older capture - obj.shunt() # Shunt capture from memory to storage - - -class CameraEvent(object): - """ - A frame-signaller object used by any instances or subclasses of BaseCamera. - - An event-like class that signals all active clients when a new frame is available. - """ - def __init__(self): - self.events = {} - - def wait(self, timeout: int = 5): - """Wait for the next frame (invoked from each client's thread).""" - ident = get_ident() - if ident not in self.events: - # this is a new client - # add an entry for it in the self.events dict - # each entry has two elements, a threading.Event() and a timestamp - self.events[ident] = [threading.Event(), time.time()] - return self.events[ident][0].wait(timeout) - - def set(self): - """Signal that a new frame is available.""" - now = time.time() - remove = None - for ident, event in self.events.items(): - if not event[0].isSet(): - # if this client's event is not set, then set it - # also update the last set timestamp to now - event[0].set() - event[1] = now - else: - # if the client's event is already set, it means the client - # did not process a previous frame - # if the event stays set for more than 5 seconds, then assume - # the client is gone and remove it - if now - event[1] > 5: - remove = ident - if remove: - del self.events[remove] - - def clear(self): - """Clear frame event, once processed.""" - self.events[get_ident()][0].clear() +from labthings import StrictLock, ClientEvent class BaseCamera(metaclass=ABCMeta): """ Base implementation of StreamingCamera. - - Attributes: - thread: Background thread reading frames from camera - camera: Camera object - lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread - access to stage hardware - frame (bytes): Current frame is stored here by background thread - last_access (time): Time of last client access to the camera - stream_timeout (int): Number of inactive seconds before timing out the stream - stream_timeout_enabled (bool): Enable or disable timing out the stream - state (dict): Dictionary for capture state - paths (dict): Dictionary of capture paths - images (list): List of image capture objects - videos (list): List of video capture objects """ + def __init__(self): self.thread = None - self.camera = None + self.camera = None - self.lock = StrictLock(timeout=1) + self.lock = StrictLock(name="Camera", timeout=None) self.frame = None self.last_access = 0 - self.event = CameraEvent() + self.event = ClientEvent() self.stop = False # Used to indicate that the stream loop should break - self.stream_timeout = 20 - self.stream_timeout_enabled = False + self.stream_active = False + self.record_active = False + self.preview_active = False - self.state = {} - self.paths = { - 'image': BASE_CAPTURE_PATH, - 'video': BASE_CAPTURE_PATH, - 'image_tmp': TEMP_CAPTURE_PATH, - 'video_tpm': TEMP_CAPTURE_PATH - } + @property + @abstractmethod + def configuration(self): + """The current camera configuration.""" + pass - # Capture data - self.images = [] - self.videos = [] + @property + @abstractmethod + def state(self): + """The current read-only camera state.""" + pass + + @property + def settings(self): + return self.read_settings() @abstractmethod - def apply_config(self, config: dict): + def update_settings(self, config: dict): """Update settings from a config dictionary""" - pass + with self.lock(timeout=None): + # Apply valid config params to camera object + for key, value in config.items(): # For each provided setting + if hasattr(self, key): # If the instance has a matching property + setattr(self, key, value) # Set to the target value @abstractmethod - def read_config(self) -> dict: + def read_settings(self) -> dict: """Return the current settings as a dictionary""" - pass + return {} def __enter__(self): """Create camera on context enter.""" @@ -159,10 +73,6 @@ class BaseCamera(metaclass=ABCMeta): def close(self): """Close the BaseCamera and all attached StreamObjects.""" logging.info("Closing {}".format(self)) - # Close all StreamObjects - for capture_list in [self.images, self.videos]: - for stream_object in capture_list: - stream_object.close() # Stop worker thread self.stop_worker() logging.info("Closed {}".format(self)) @@ -176,8 +86,8 @@ class BaseCamera(metaclass=ABCMeta): self.last_access = time.time() self.stop = False - if not self.state['stream_active']: - # start background frame thread + if not self.stream_active: + # Start background frame thread self.thread = threading.Thread(target=self._thread) self.thread.daemon = True self.thread.start() @@ -196,12 +106,12 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("Stopping worker thread") timeout_time = time.time() + timeout - if self.state['stream_active']: + if self.stream_active: self.stop = True self.thread.join() # Wait for stream thread to exit logging.debug("Waiting for stream thread to exit.") - while self.state['stream_active']: + while self.stream_active: if time.time() > timeout_time: logging.debug("Timeout waiting for worker thread close.") raise TimeoutError("Timeout waiting for worker thread close.") @@ -226,118 +136,6 @@ class BaseCamera(metaclass=ABCMeta): """Create generator that returns frames from the camera.""" pass - # RETURNING CAPTURES - - @property - def image(self): - """Return the latest captured image.""" - return last_entry(self.images) - - @property - def video(self): - """Return the latest recorded video.""" - return last_entry(self.videos) - - def image_from_id(self, image_id): - """Return an image StreamObject with a matching ID.""" - return entry_by_id(image_id, self.images) - - def video_from_id(self, video_id): - """Return a video StreamObject with a matching ID.""" - return entry_by_id(video_id, self.videos) - - # CREATING NEW CAPTURES - - def new_image( - self, - write_to_file: bool = True, - temporary: bool = True, - filename: str = None, - folder: str = "", - fmt: str = 'jpeg'): - - """ - Create a new image capture object. Adds to the image list, and shunt all others. - - Args: - write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream. - temporary (bool): Should the data be deleted after session ends. - Creating the capture with a content manager sets this to true. - filename (str): Name of the stored file. Defaults to timestamp. - folder (str): Name of the folder in which to store the capture. - fmt (str): Format of the capture. - """ - - # Generate file name - if not filename: - filename = generate_numbered_basename(self.images) - logging.debug(filename) - filename = "{}.{}".format(filename, fmt) - - # Generate folder - base_folder = self.paths['image_tmp'] if temporary else self.paths['image'] - folder = os.path.join(base_folder, folder) - - # Generate file path - filepath = os.path.join(folder, filename) - - # Create capture object - output = CaptureObject( - write_to_file=write_to_file, - temporary=temporary, - filepath=filepath) - - # Update capture list - shunt_captures(self.images) - self.images.append(output) - - return output - - def new_video( - self, - write_to_file: bool = True, - temporary: bool = False, - filename: str = None, - folder: str = "", - fmt: str = 'h264'): - - """ - Create a new video capture object. Adds to the image list, and shunt all others. - - Args: - write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream. - temporary (bool): Should the data be deleted after session ends. - Creating the capture with a content manager sets this to true. - filename (str): Name of the stored file. Defaults to timestamp. - folder (str): Name of the folder in which to store the capture. - fmt (str): Format of the capture. - """ - - # Generate file name - if not filename: - filename = generate_numbered_basename(self.videos) - logging.debug(filename) - filename = "{}.{}".format(filename, fmt) - - # Generate folder - base_folder = self.paths['video_tmp'] if temporary else self.paths['video'] - folder = os.path.join(base_folder, folder) - - # Generate file path - filepath = os.path.join(folder, filename) - - # Create capture object - output = CaptureObject( - write_to_file=write_to_file, - temporary=temporary, - filepath=filepath) - - # Update capture list - shunt_captures(self.videos) - self.videos.append(output) - - return output - # WORKER THREAD def _thread(self): @@ -345,21 +143,11 @@ class BaseCamera(metaclass=ABCMeta): self.frames_iterator = self.frames() logging.debug("Entering worker thread.") - self.state['stream_active'] = True + self.stream_active = True for frame in self.frames_iterator: self.frame = frame self.event.set() # send signal to clients - time.sleep(0) - - # Handle timeout - if ( - self.stream_timeout_enabled and # If using timeout - (time.time() - self.last_access > self.stream_timeout) and # And timeout time - not self.state['preview_active'] # And GPU preview is not active - ): - self.frames_iterator.close() - break try: if self.stop is True: @@ -372,4 +160,4 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("BaseCamera worker thread exiting...") # Set stream_activate state - self.state['stream_active'] = False + self.stream_active = False diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py deleted file mode 100644 index ea789088..00000000 --- a/openflexure_microscope/camera/capture.py +++ /dev/null @@ -1,479 +0,0 @@ -import uuid -import io -import os -import shutil -import glob -import datetime -import yaml -import logging -from PIL import Image -import atexit - -from openflexure_microscope.camera import piexif -from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError - -""" -Attributes: - BASE_CAPTURE_PATH (str): Base path to store all captures - TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied) -""" - -PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] -EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF'] -THUMBNAIL_SIZE = (200, 150) - -BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') -TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') - - -# TODO: Move these methods to a camera utilities module? -def clear_tmp(): - """ - Removes all files in the ``TEMP_CAPTURE_PATH`` directory - """ - global TEMP_CAPTURE_PATH - - if os.path.isdir(TEMP_CAPTURE_PATH): - logging.info("Clearing {}...".format(TEMP_CAPTURE_PATH)) - shutil.rmtree(TEMP_CAPTURE_PATH) - logging.debug("Cleared {}.".format(TEMP_CAPTURE_PATH)) - - -def pull_usercomment_dict(filepath): - """ - Reads UserComment Exif data from a file, and returns the contained bytes as a dictionary. - Args: - filepath: Path to the Exif-containing file - """ - try: - exif_dict = piexif.load(filepath) - except InvalidImageDataError: - logging.error("Invalid data at {}. Skipping.".format(filepath)) - return None - if 'Exif' in exif_dict and 37510 in exif_dict['Exif']: - return yaml.load(exif_dict['Exif'][37510].decode()) - else: - return None - - -def make_file_list(directory, formats): - files = [] - for fmt in formats: - files.extend(glob.glob('{}/**/*.{}'.format(directory, fmt.lower()), recursive=True)) - - logging.info("{} capture files found on disk".format(len(files))) - - return files - - -def build_captures_from_exif(): - global BASE_CAPTURE_PATH, EXIF_FORMATS - - logging.debug("Reloading captures from {}...".format(BASE_CAPTURE_PATH)) - files = make_file_list(BASE_CAPTURE_PATH, EXIF_FORMATS) - captures = [] - - for f in files: - logging.debug("Reloading capture {}...".format(f)) - exif = pull_usercomment_dict(f) - if exif: - capture = capture_from_exif(f, exif) - captures.append(capture) - else: - logging.error("Invalid data at {}. Skipping.".format(f)) - - logging.info("{} capture files successfully reloaded".format(len(captures))) - - return captures - - -def capture_from_exif(path, exif_dict): - """ - Creates an instance of CaptureObject from a dictionary of capture information. - This is used when reloading the API server, to restore captures created in the - previous session. - - Args: - exif_dict (dict): Dictionary containing capture information - """ - - # Create a placeholder capture - capture = CaptureObject( - filepath=path - ) - - # Build file path information - capture.split_file_path(capture.file) - - # Populate capture parameters - capture.id = exif_dict['id'] - - capture.timestring = exif_dict['time'] - capture.format = exif_dict['format'] - - capture._metadata = exif_dict['custom'] - capture.tags = exif_dict['tags'] - - return capture - - -class CaptureObject(object): - """ - StreamObject used to store and process capture data, and metadata. - - Attributes: - timestring (str): Timestring of capture creation time - temporary (bool): Mark the capture as temporary, to be deleted as the server closes, - or as resources are required - _metadata (dict): Dictionary of custom metadata to be included in metadata file - tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation - bytestream (:py:class:`io.BytesIO`): Byte bytestream that data will be written to - filefolder (str): Folder in which the capture file will be stored - filename (str): Full name of the capture file - basename (str): Filename of the capture, without a file extension - format (str): Format of the capture data - - Notes: - Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH. - """ - - def __init__( - self, - write_to_file: bool = False, - temporary: bool = False, - filepath: str = '') -> None: - """Create a new StreamObject, to manage capture data.""" - - # Store a nice ID - self.id = uuid.uuid4().hex #: str: Unique capture ID - logging.debug("Created StreamObject {}".format(self.id)) - self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - - # Keep on disk after close by default - self.temporary = temporary - - # Create file name. Default to UUID - if not filepath: - self.file = self.build_file_path() - else: - self.file = filepath - self.split_file_path(self.file) - - # Dictionary for storing custom metadata - self._metadata = {} - - # List for storing tags - self.tags = [] - - # Byte bytestream properties - self.bytestream = io.BytesIO() - - # Set default write target - if not write_to_file: - self.stream = self.bytestream - else: - self.stream = self.file - - # Log if created by context manager - self.context_manager = False - - # Thumbnail (populated only for PIL captures) - self.thumb_bytes = None - - def __enter__(self): - """Create StreamObject in context, to auto-clean disk data.""" - logging.debug( - "Entering context for {}. Stored files will be cleaned up automatically regardless of location.".format( - self.id)) - self.temporary = True # Flag file to be removed on close. - self.context_manager = True # Used in metadata - - logging.info("Rebuilding as a temporary capture...") - self.build_file_path() - - return self - - def __exit__(self, *args): - """Exit StreamObject, and auto-clean disk data.""" - logging.info("Cleaning up {}".format(self.id)) - self.close() - - def build_file_path(self): - """ - Construct a full file path, based on filename, folder, and file format. - Defaults to UUID. - """ - global TEMP_CAPTURE_PATH, BASE_CAPTURE_PATH - if self.temporary: - base_dir = TEMP_CAPTURE_PATH - else: - base_dir = BASE_CAPTURE_PATH - return os.path.join(base_dir, self.filename) # Full file name by joining given folder to given name - - def split_file_path(self, filepath): - """ - Take a full file path, and split it into separated class properties. - - Args: - filepath (str): String of the full file path, including file format extension - """ - # Split the full file path into a folder and a filename - self.filefolder, self.filename = os.path.split(filepath) - # Split the filename out from it's file extension - self.basename = os.path.splitext(self.filename)[0] - self.format = self.filename.split('.')[-1] - - # Create folder and file - if not os.path.exists(self.filefolder): - os.makedirs(self.filefolder) - - @property - def stream_exists(self, auto_rewind=True) -> bool: - """Check if capture data BytesIO bytestream exists in memory.""" - if auto_rewind: - self.bytestream.seek(0) # Rewind the data bytes for reading - if self.bytestream.getvalue(): # If data bytestream contains data - self.bytestream.seek(0) # Rewind the data bytes for reading - return True - else: - self.bytestream.seek(0) # Rewind the data bytes for reading - return False - - @property - def file_exists(self) -> bool: - """Check if capture data file exists on disk.""" - if os.path.isfile(self.file): - return True - else: - return False - - # HANDLE TAGS - def put_tags(self, tags: list): - """ - Add a new tag to the ``tags`` list attribute. - - Args: - tags (list): List of tags to be added - """ - for tag in tags: - if tag not in self.tags: - self.tags.append(tag) - - self.save_metadata() - - def delete_tag(self, tag: str): - """ - Remove a tag from the ``tags`` list attribute, if it exists. - - Args: - tag (str): Tag to be removed - """ - if tag in self.tags: - self.tags = [new_tag for new_tag in self.tags if new_tag != tag] - - self.save_metadata() - - # HANDLE METADATA - - def put_metadata(self, data: dict) -> None: - """ - Merge metadata from a passed dictionary into the capture metadata, and saves. - - Args: - data (dict): Dictionary of metadata to be added - """ - self._metadata.update(data) - self.save_metadata() - - def save_metadata(self) -> None: - """ - Save metadata to exif, if supported - """ - global EXIF_FORMATS - - if self.format.upper() in EXIF_FORMATS and self.file_exists: - logging.debug("Writing exif data to capture file") - # Extract current Exif data - exif_dict = piexif.load(self.file) - # Serialize metadata - metadata_string = yaml.safe_dump(self.metadata) - # Insert metadata into exif_dict - exif_dict['Exif'][piexif.ExifIFD.UserComment] = metadata_string.encode() - # Convert new exif dict to exif bytes - exif_bytes = piexif.dump(exif_dict) - # Insert exif into file - piexif.insert(exif_bytes, self.file) - - @property - def metadata(self) -> dict: - """ - Create basic metadata dictionary from basic capture data, - and any added custom metadata and tags. - """ - d = {'id': self.id, 'filename': self.filename, 'time': self.timestring, - 'format': self.format, 'tags': self.tags, 'custom': self._metadata} - - # Add custom metadata to dictionary - return d - - @property - def yaml(self) -> str: - """ - Return a string containing a YAML-formatted representation of the capture matadata - """ - return yaml.dump(self.metadata, default_flow_style=False) - - @property - def exists(self) -> bool: - """ - Check if either an in-memory byte stream or on-disk file of capture data exists. - - If False, the capture data cannot be obtained. - """ - return self.stream_exists or self.file_exists - - @property - def state(self) -> dict: - """ - Return a dictionary of objects full state, including metadata. - """ - - # Create basic state dictionary - d = {'path': self.file, 'temporary': self.temporary, 'metadata': self.metadata} - - # Check bytestream - if self.stream_exists: - d['bytestream'] = True - else: - d['bytestream'] = False - - # Combined availability of data - if self.exists: - d['available'] = True - else: - d['available'] = False - - return d - - @property - def data(self) -> io.BytesIO: - """ - Return a byte string of the capture data. - - If the capture data exists in-memory, this will be loaded. If the data only exists - on disk, this will automatically fall back to loading from disk. - """ - self.bytestream.seek(0) # Rewind the data bytes for reading - - if self.stream_exists: # If data bytestream contains data - # Create a copy of the bytestream bytes - logging.debug("STREAM EXISTS") - data = io.BytesIO(self.bytestream.getbuffer()) - - else: # If data bytestream is empty - if self.file_exists: # If data file exists - logging.info("Opening from file {}".format(self.file)) - with open(self.file, 'rb') as f: - d = io.BytesIO(f.read()) # Load bytes from file - d.seek(0) # Rewind loaded bytestream - # Create a copy of the bytestream bytes - data = io.BytesIO(d.getbuffer()) - else: - data = None - - return data # Read and return bytes data - - @property - def binary(self) -> bytes: - """Return a byte string of the capture data.""" - return self.data.getvalue() - - @property - def thumbnail(self) -> io.BytesIO: - """ - Returns a thumbnail of the capture data, for supported image formats. - """ - global THUMBNAIL_SIZE - # If no thumbnail exists, try and make one - if not self.thumb_bytes: - logging.info("Building thumbnail") - self.thumb_bytes = io.BytesIO() - if self.format.upper() in PIL_FORMATS: - im = Image.open(self.data) - im.thumbnail(THUMBNAIL_SIZE) - im.save(self.thumb_bytes, self.format) - self.thumb_bytes.seek(0) - else: - self.thumb_bytes.seek(0) - - # Copy the buffer, to avoid closing the file - data = io.BytesIO(self.thumb_bytes.getbuffer()) - return data - - def load_file(self) -> bool: - """Load data stored on disk to the in-memory bytestream.""" - if self.file_exists: # If data file exists - with open(self.file, 'rb') as f: - self.bytestream = io.BytesIO(f.read()) # Load bytes from file - self.bytestream.seek(0) # Rewind data bytes again - return True - else: - return False - - def save_file(self) -> bool: - """Write the StreamObjects bytestream to a file.""" - if self.stream_exists: # If there's a bytestream to save - with open(self.file, 'ab') as f: # Load file as bytes - logging.debug("Writing bytestream to file {}".format(self.file)) - f.seek(0, 0) # Seek to the start of the file - f.write(self.binary) # Write data bytes to file - return True - else: - return False - - def save(self) -> None: - """Write stream to file, and save/update metadata file""" - # Try to save the file (only succeeds if an unsaved stream exists) - self.save_file() - - # If a stream OR file exists, save the metadata file - if self.exists: - self.save_metadata() - - def delete_stream(self): - """Clear the BytesIO bytestream of the StreamObject.""" - self.bytestream = io.BytesIO() - - def delete_file(self) -> bool: - """If the StreamObject has been saved, delete the file.""" - - if os.path.isfile(self.file): - logging.info("Deleting file {}".format(self.file)) - os.remove(self.file) - - return True - else: - return False - - def delete(self): - """Entirely delete all capture data.""" - logging.info("Deleting {}".format(self.id)) - self.delete_stream() - self.delete_file() - - def shunt(self): - """Demote the StreamObject from being stored in memory.""" - if not self.file_exists: # If file doesn't already exist - self.save() # Save bytestream to disk, if it exists - self.delete_stream() # Delete the bytestream from memory - - def close(self): - """Both clear the bytestream, and delete any associated on-disk data.""" - logging.info("Closing {}".format(self.id)) - self.delete_stream() - # Delete the file from disk if temporary - if self.temporary: - self.delete() - - -atexit.register(clear_tmp) diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py index 9b6103af..0b858cda 100644 --- a/openflexure_microscope/camera/mock.py +++ b/openflexure_microscope/camera/mock.py @@ -8,29 +8,32 @@ from __future__ import division import io import time import numpy as np -from PIL import Image +from PIL import Image, ImageFont, ImageDraw +from datetime import datetime import logging # Type hinting from typing import Tuple -from .base import BaseCamera, CaptureObject +from openflexure_microscope.camera.base import BaseCamera +from openflexure_microscope.captures import CaptureObject + + +""" +PIL spams the logger with debug-level information. This is a pain when debugging api.app. +We override the logging settings in api.app by setting a level for PIL here. +""" +pil_logger = logging.getLogger("PIL") +pil_logger.setLevel(logging.INFO) # MAIN CLASS -class MockStreamer(BaseCamera): - +class MissingCamera(BaseCamera): def __init__(self): # Run BaseCamera init BaseCamera.__init__(self) - # Store state of PiCameraStreamer - self.state.update({ - 'stream_active': False, - 'record_active': False - }) - # Update config properties self.image_resolution = (1312, 976) self.stream_resolution = (640, 480) @@ -41,9 +44,40 @@ class MockStreamer(BaseCamera): # Create an empty stream self.stream = io.BytesIO() + # Generate an initial dummy image + self.generate_new_dummy_image() + # Start streaming self.start_worker() + def generate_new_dummy_image(self): + # Create a dummy image to serve in the stream + image = Image.new( + "RGB", + (self.stream_resolution[0], self.stream_resolution[1]), + color=(0, 0, 0), + ) + + draw = ImageDraw.Draw(image) + draw.text( + (20, 70), + "Camera disconnected: {}".format( + datetime.now().strftime("%d/%m/%Y, %H:%M:%S") + ), + ) + + image.save(self.stream, format="JPEG") + + @property + def configuration(self): + """The current camera configuration.""" + return {} + + @property + def state(self): + """The current read-only camera state.""" + return {} + def initialisation(self): """Run any initialisation code when the frame iterator starts.""" pass @@ -54,21 +88,27 @@ class MockStreamer(BaseCamera): BaseCamera.close(self) # HANDLE SETTINGS - def read_config(self) -> dict: + def read_settings(self) -> dict: """ Return config dictionary of the PiCameraStreamer. """ - conf_dict = { - 'stream_resolution': self.stream_resolution, - 'image_resolution': self.image_resolution, - 'numpy_resolution': self.numpy_resolution, - 'jpeg_quality': self.jpeg_quality - } + # Get config items from the base class + conf_dict = BaseCamera.read_settings(self) + + # Include device-specific config items + conf_dict.update( + { + "stream_resolution": self.stream_resolution, + "image_resolution": self.image_resolution, + "numpy_resolution": self.numpy_resolution, + "jpeg_quality": self.jpeg_quality, + } + ) return conf_dict - def apply_config(self, config: dict): + def update_settings(self, config: dict): """ Write a config dictionary to the PiCameraStreamer config. @@ -78,32 +118,24 @@ class MockStreamer(BaseCamera): Args: config (dict): Dictionary of config parameters. """ - # TODO: Include timing and batching logic when applying PiCamera settings - - paused_stream = False - logging.debug("PiCameraStreamer: Applying config:") + logging.debug("MockStreamer: Applying config:") logging.debug(config) with self.lock: - # Apply valid config params to Picamera object - if not self.state['record_active']: # If not recording a video + # Apply valid config params to camera object + if not self.record_active: # If not recording a video - # PiCameraStreamer parameters for key, value in config.items(): # For each provided setting if hasattr(self, key): setattr(self, key, value) - # If stream was paused to update config, unpause - if paused_stream: - logging.info("Resuming stream.") - self.start_stream_recording() - else: raise Exception( - "Cannot update camera config while recording is active.") + "Cannot update camera config while recording is active." + ) - def set_zoom(self, zoom_value: float = 1.) -> None: + def set_zoom(self, zoom_value: float = 1.0) -> None: """ Change the camera zoom, handling re-centering and scaling. """ @@ -119,18 +151,13 @@ class MockStreamer(BaseCamera): """Stop the on board GPU camera preview.""" logging.warning("GPU preview not implemented in mock camera") - - def start_recording( - self, - output, - fmt: str = 'h264', - quality: int = 15): + def start_recording(self, output, fmt: str = "h264", quality: int = 15): """Start recording. Start a new video recording, writing to a output object. Args: - output (CaptureObject/str): Output object to write data bytes to. + output: String or file-like object to write capture data to fmt (str): Format of the capture. quality (int): Video recording quality. @@ -142,19 +169,19 @@ class MockStreamer(BaseCamera): # Start recording method only if a current recording is not running logging.warning("Recording not implemented in mock camera") - def stop_recording(self): """Stop the last started video recording on splitter port 2.""" with self.lock: logging.warning("Recording not implemented in mock camera") def capture( - self, - output, - fmt: str = 'jpeg', - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = True): + self, + output, + fmt: str = "jpeg", + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = True, + ): """ Capture a still image to a StreamObject. @@ -162,7 +189,7 @@ class MockStreamer(BaseCamera): Target object can be overridden for development purposes. Args: - output (CaptureObject/str): Output object to write data bytes to. + output: String or file-like object to write capture data to use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. fmt (str): Format of the capture. resize ((int, int)): Resize the captured image. @@ -170,22 +197,19 @@ class MockStreamer(BaseCamera): """ with self.lock: - logging.warning("Capture not implemented in mock camera") + if isinstance(output, str): + output = open(output, "wb") + output.write(self.stream.getvalue()) - def gen_img(self): - imarray = np.random.rand(self.stream_resolution[1], self.stream_resolution[0], 3) * 255 - im = Image.fromarray(imarray.astype('uint8')).convert('L') - im.save(self.stream, format="JPEG") + if isinstance(output, str): + output.close() # HANDLE STREAM FRAMES def frames(self): """ Create generator that returns frames from the camera. - - Records video from port 1 to a byte stream, - and iterates sequential frames. """ # Run this initialisation method self.initialisation() @@ -196,14 +220,14 @@ class MockStreamer(BaseCamera): # While the iterator is not closed try: while True: - # reset stream for next frame + time.sleep(1) # Only serve frames at 1fps + # Reset stream self.stream.seek(0) self.stream.truncate() - # to stream, read the new frame - time.sleep(1 / self.framerate * 0.1) - # yield the result to be read - self.gen_img() + # Generate new dumm image + self.generate_new_dummy_image() + # Get frame data frame = self.stream.getvalue() # ensure the size of package is right diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index cf2d70c6..fdf8576d 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -3,17 +3,22 @@ """ Raspberry Pi camera implementation of the PiCameraStreamer class. -NOTES: -Still port used for image capture -Preview port reserved for onboard GPU preview +**NOTES:** + +Still port used for image capture. +Preview port reserved for onboard GPU preview. + Video port: - Splitter port 0: Image capture (if use_video_port == True) - Splitter port 1: Streaming frames - Splitter port 2: Video capture - Splitter port 3: [Currently unused] + +* Splitter port 0: Image capture (if `use_video_port == True`) +* Splitter port 1: Streaming frames +* Splitter port 2: Video capture +* Splitter port 3: [Currently unused] PiCameraStreamer streams at video_resolution + Camera capture resolution set to stream_resolution in frames() + Video port uses that resolution for everything. If a different resolution is specified for video capture, this is handled by the resizer. @@ -26,6 +31,7 @@ from __future__ import division import io import time import numpy as np +import os import logging # Pi camera @@ -35,53 +41,94 @@ import picamera.array # Type hinting from typing import Tuple -from .base import BaseCamera, CaptureObject +from openflexure_microscope.camera.base import BaseCamera +from openflexure_microscope.captures import CaptureObject + # Richard's fix gain from .set_picamera_gain import set_analog_gain, set_digital_gain +from openflexure_microscope.paths import settings_file_path +from openflexure_microscope.utilities import ( + serialise_array_b64, + ndarray_to_json, + json_to_ndarray, +) + # MAIN CLASS class PiCameraStreamer(BaseCamera): """Raspberry Pi camera implementation of PiCameraStreamer.""" + picamera_settings_keys = [ - 'exposure_mode', - 'analog_gain', - 'digital_gain', - 'shutter_speed', - 'awb_gains', - 'awb_mode', - 'framerate', - 'saturation', - 'lens_shading_table' + "exposure_mode", + "analog_gain", + "digital_gain", + "shutter_speed", + "awb_gains", + "awb_mode", + "framerate", + "saturation", + "iso", + "brightness", + "contrast", + "crop", + "drc_strength", + "exposure_compensation", + "image_effect", + "meter_mode", + "sharpness", + "annotate_text", + "annotate_text_size", + "zoom", ] def __init__(self): # Run BaseCamera init BaseCamera.__init__(self) # Attach to Pi camera - self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object + self.camera = ( + picamera.PiCamera() + ) #: :py:class:`picamera.PiCamera`: Picamera object # Store state of PiCameraStreamer - self.state.update({ - 'stream_active': False, - 'record_active': False, - 'preview_active': False, - }) + self.preview_active = False + # Reset variable states self.set_zoom(1.0) - # Update config properties - self.image_resolution = tuple(self.camera.MAX_RESOLUTION) - self.stream_resolution = (832, 624) - self.numpy_resolution = (1312, 976) - self.jpeg_quality = 75 + # Set default settings + self.image_resolution = tuple( + self.camera.MAX_RESOLUTION + ) #: tuple: Resolution for image captures + self.stream_resolution = ( + 832, + 624, + ) #: tuple: Resolution for stream and video captures + self.numpy_resolution = ( + 1312, + 976, + ) #: tuple: Resolution for numpy array captures + self.jpeg_quality = 100 #: int: JPEG quality + self.mjpeg_quality = 75 #: int: MJPEG quality - # Create an empty stream - self.stream = io.BytesIO() - - # Start streaming + # Set default lens shading table path + self.picamera_lst_path = settings_file_path( + "picamera_lst.npy" + ) #: str: Path of .npy lens shading table file + + # Start the stream worker on init self.start_worker() + @property + def configuration(self): + """The current camera configuration.""" + return {"board": self.camera.revision} + + @property + def state(self): + """The current read-only camera state.""" + return {} + def initialisation(self): """Run any initialisation code when the frame iterator starts.""" pass @@ -95,31 +142,47 @@ class PiCameraStreamer(BaseCamera): self.camera.close() # HANDLE SETTINGS - def read_config(self) -> dict: + def read_settings(self) -> dict: """ Return config dictionary of the PiCameraStreamer. """ - conf_dict = { - 'stream_resolution': self.stream_resolution, - 'image_resolution': self.image_resolution, - 'numpy_resolution': self.numpy_resolution, - 'jpeg_quality': self.jpeg_quality, - 'picamera_settings': {}, - } + # Get config items from the base class + conf_dict = BaseCamera.read_settings(self) - # PiCamera parameters + # Include device-specific config items + conf_dict.update( + { + "stream_resolution": self.stream_resolution, + "image_resolution": self.image_resolution, + "numpy_resolution": self.numpy_resolution, + "jpeg_quality": self.jpeg_quality, + "mjpeg_quality": self.mjpeg_quality, + "picamera": {}, + } + ) + + # Include a subset of picamera properties. Excludes lens shading table for key in PiCameraStreamer.picamera_settings_keys: try: value = getattr(self.camera, key) logging.debug("Reading PiCamera().{}: {}".format(key, value)) - conf_dict['picamera_settings'][key] = value + conf_dict["picamera"][key] = value except AttributeError: logging.debug("Unable to read PiCamera attribute {}".format(key)) + # Include a serialised lens shading table + if ( + hasattr(self.camera, "lens_shading_table") + and getattr(self.camera, "lens_shading_table") is not None + ): + conf_dict["picamera"]["lens_shading_table"] = ndarray_to_json( + getattr(self.camera, "lens_shading_table") + ) + return conf_dict - def apply_config(self, config: dict): + def update_settings(self, config: dict): """ Write a config dictionary to the PiCameraStreamer config. @@ -129,30 +192,43 @@ class PiCameraStreamer(BaseCamera): Args: config (dict): Dictionary of config parameters. """ - # TODO: Include timing and batching logic when applying PiCamera settings paused_stream = False logging.debug("PiCameraStreamer: Applying config:") logging.debug(config) - with self.lock: + with self.lock(timeout=None): # Apply valid config params to Picamera object - if not self.state['record_active']: # If not recording a video + if not self.record_active: # If not recording a video # Pause stream while changing settings - if self.state['stream_active']: # If stream is active + if self.stream_active: # If stream is active logging.info("Pausing stream to update config.") self.stop_stream_recording() # Pause stream paused_stream = True # Remember to unpause stream when done # PiCamera parameters - if 'picamera_settings' in config: # If new settings are given - self.apply_picamera_settings(config['picamera_settings'], pause_for_effect=True) + if "picamera" in config: # If new settings are given + self.apply_picamera_settings( + config["picamera"], pause_for_effect=True + ) + + # Handle lens shading if camera supports it + if ( + hasattr(self.camera, "lens_shading_table") + and "lens_shading_table" in config["picamera"] + ): + try: + self.camera.lens_shading_table = json_to_ndarray( + config["picamera"].get("lens_shading_table") + ) + except KeyError as e: + logging.error(e) # PiCameraStreamer parameters for key, value in config.items(): # For each provided setting - if (key != 'picamera_settings') and hasattr(self, key): + if (key != "picamera") and hasattr(self, key): setattr(self, key, value) # If stream was paused to update config, unpause @@ -162,67 +238,79 @@ class PiCameraStreamer(BaseCamera): else: raise Exception( - "Cannot update camera config while recording is active.") + "Cannot update camera config while recording is active." + ) - def apply_picamera_settings(self, settings_dict: dict, pause_for_effect: bool=True): + def apply_picamera_settings( + self, settings_dict: dict, pause_for_effect: bool = True + ): + """ + + Args: + settings_dict (dict): Dictionary of properties to apply to the :py:class:`picamera.PiCamera`: object + pause_for_effect (bool): Pause tactically to reduce risk of timing issues + """ # Set exposure mode - if 'exposure_mode' in settings_dict: - logging.debug("Applying exposure_mode: {}".format(settings_dict['exposure_mode'])) - self.camera.exposure_mode = settings_dict['exposure_mode'] + if "exposure_mode" in settings_dict: + logging.debug( + "Applying exposure_mode: {}".format(settings_dict["exposure_mode"]) + ) + self.camera.exposure_mode = settings_dict["exposure_mode"] # Apply gains and let them settle - if 'analog_gain' in settings_dict: - logging.debug("Applying analog_gain: {}".format(settings_dict['analog_gain'])) - set_analog_gain(self.camera, float(settings_dict['analog_gain'])) - if 'digital_gain' in settings_dict: - logging.debug("Applying digital_gain: {}".format(settings_dict['digital_gain'])) - set_digital_gain(self.camera, float(settings_dict['digital_gain'])) - + if "analog_gain" in settings_dict: + logging.debug( + "Applying analog_gain: {}".format(settings_dict["analog_gain"]) + ) + set_analog_gain(self.camera, float(settings_dict["analog_gain"])) + if "digital_gain" in settings_dict: + logging.debug( + "Applying digital_gain: {}".format(settings_dict["digital_gain"]) + ) + set_digital_gain(self.camera, float(settings_dict["digital_gain"])) + # Apply shutter speed - if 'shutter_speed' in settings_dict: - logging.debug("Applying shutter_speed: {}".format(settings_dict['shutter_speed'])) - self.camera.shutter_speed = int(settings_dict['shutter_speed']) + if "shutter_speed" in settings_dict: + logging.debug( + "Applying shutter_speed: {}".format(settings_dict["shutter_speed"]) + ) + self.camera.shutter_speed = int(settings_dict["shutter_speed"]) time.sleep(0.2) # Let gains settle # Handle AWB in a half-smart way - if 'awb_gains' in settings_dict: + if "awb_gains" in settings_dict: logging.debug("Applying awb_mode: off") - self.camera.awb_mode = 'off' - logging.debug("Applying awb_gains: {}".format(settings_dict['awb_gains'])) - self.camera.awb_gains = settings_dict['awb_gains'] - elif 'awb_mode' in settings_dict: - logging.debug("Applying awb_mode: {}".format(settings_dict['awb_mode'])) - self.camera.awb_mode = settings_dict['awb_mode'] + self.camera.awb_mode = "off" + logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"])) + self.camera.awb_gains = settings_dict["awb_gains"] + elif "awb_mode" in settings_dict: + logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"])) + self.camera.awb_mode = settings_dict["awb_mode"] # Handle some properties that can be quickly applied - batched_keys = ['framerate', 'saturation'] + batched_keys = ["framerate", "saturation"] for key in batched_keys: if (key in settings_dict) and hasattr(self.camera, key): logging.debug("Applying {}: {}".format(key, settings_dict[key])) setattr(self.camera, key, settings_dict[key]) - # Handle lens shading if camera supports it - if ('lens_shading_table' in settings_dict) and hasattr(self.camera, 'lens_shading_table'): - logging.debug("Applying lens_shading_table: {}".format(settings_dict['lens_shading_table'])) - self.camera.lens_shading_table = settings_dict['lens_shading_table'] - # Final optional pause to settle if pause_for_effect: time.sleep(0.2) - def set_zoom(self, zoom_value: float = 1.) -> None: + def set_zoom(self, zoom_value: float = 1.0) -> None: """ Change the camera zoom, handling re-centering and scaling. """ - with self.lock: - self.state['zoom_value'] = float(zoom_value) - if self.state['zoom_value'] < 1: - self.state['zoom_value'] = 1 + with self.lock(timeout=None): + self.zoom_value = float(zoom_value) + if self.zoom_value < 1: + self.zoom_value = 1 # Richard's code for zooming ! fov = self.camera.zoom centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0]) - size = 1.0 / self.state['zoom_value'] + size = 1.0 / self.zoom_value # If the new zoom value would be invalid, move the centre to # keep it within the camera's sensor (this is only relevant # when zooming out, if the FoV is not centred on (0.5, 0.5) @@ -239,41 +327,43 @@ class PiCameraStreamer(BaseCamera): """Start the on board GPU camera preview.""" logging.info("Starting the GPU preview") - # TODO: Commented out as I honestly can't remember why this was here. May need to put back? - # self.start_stream_recording() - - try: - if not self.camera.preview: - logging.debug("Starting preview") - self.camera.start_preview(fullscreen=fullscreen, window=window) - else: - logging.debug("Resizing preview") - if window: - self.camera.preview.window = window - if fullscreen: - self.camera.preview.fullscreen = fullscreen - self.state['preview_active'] = True - except picamera.exc.PiCameraMMALError as e: - logging.error("Suppressed a MMALError in start_preview. Exception: {}".format(e)) - except picamera.exc.PiCameraValueError as e: - logging.error("Suppressed a ValueError exception in start_preview. Exception: {}".format(e)) + with self.lock(): + try: + if not self.camera.preview: + logging.debug("Starting preview") + self.camera.start_preview(fullscreen=fullscreen, window=window) + else: + logging.debug("Resizing preview") + if window: + self.camera.preview.window = window + if fullscreen: + self.camera.preview.fullscreen = fullscreen + self.preview_active = True + except picamera.exc.PiCameraMMALError as e: + logging.error( + "Suppressed a MMALError in start_preview. Exception: {}".format(e) + ) + except picamera.exc.PiCameraValueError as e: + logging.error( + "Suppressed a ValueError exception in start_preview. Exception: {}".format( + e + ) + ) def stop_preview(self): """Stop the on board GPU camera preview.""" - self.camera.stop_preview() - self.state['preview_active'] = False + with self.lock(): + if self.camera.preview: + self.camera.stop_preview() + self.preview_active = False - def start_recording( - self, - output, - fmt: str = 'h264', - quality: int = 15): + def start_recording(self, output, fmt: str = "h264", quality: int = 15): """Start recording. Start a new video recording, writing to a output object. Args: - output (CaptureObject/str): Output object to write data bytes to. + output: String or file-like object to write capture data to fmt (str): Format of the capture. quality (int): Video recording quality. @@ -283,34 +373,29 @@ class PiCameraStreamer(BaseCamera): """ with self.lock: # Start recording method only if a current recording is not running - if not self.state['record_active']: - - # If output is a StreamObject - if isinstance(output, CaptureObject): - # Set target to capture stream - output_stream = output.stream - else: - output_stream = output + if not self.record_active: # Start the camera video recording on port 2 logging.info("Recording to {}".format(output)) self.camera.start_recording( - output_stream, + output, format=fmt, splitter_port=2, resize=self.stream_resolution, - quality=quality) + quality=quality, + ) - # Update state dictionary - self.state['record_active'] = True + # Update state + self.record_active = True return output else: - logging.error( + logging.warning( "Cannot start a new recording\ - until the current recording has stopped.") + until the current recording has stopped." + ) return None def stop_recording(self): @@ -321,13 +406,12 @@ class PiCameraStreamer(BaseCamera): self.camera.stop_recording(splitter_port=2) logging.info("Recording stopped") - # Update state dictionary - self.state['record_active'] = False + # Update state + self.record_active = False def stop_stream_recording( - self, - splitter_port: int = 1, - resolution: Tuple[int, int] = None) -> None: + self, splitter_port: int = 1, resolution: Tuple[int, int] = None + ) -> None: """ Sets the camera resolution to the still-image resolution, and stops recording if the stream is active. @@ -346,16 +430,21 @@ class PiCameraStreamer(BaseCamera): except picamera.exc.PiCameraNotRecording: logging.info("Not recording on splitter_port {}".format(splitter_port)) else: - logging.info("Stopped MJPEG stream on port {1}. Switching to {0}.".format(resolution, splitter_port)) + logging.info( + "Stopped MJPEG stream on port {1}. Switching to {0}.".format( + resolution, splitter_port + ) + ) # Increase the resolution for taking an image - time.sleep(0.2) # Sprinkled a sleep to prevent camera getting confused by rapid commands + time.sleep( + 0.2 + ) # Sprinkled a sleep to prevent camera getting confused by rapid commands self.camera.resolution = resolution def start_stream_recording( - self, - splitter_port: int = 1, - resolution: Tuple[int, int] = None) -> None: + self, splitter_port: int = 1, resolution: Tuple[int, int] = None + ) -> None: """ Sets the camera resolution to the video/stream resolution, and starts recording if the stream should be active. @@ -364,47 +453,59 @@ class PiCameraStreamer(BaseCamera): resolution ((int, int)): Resolution to set the camera to, before starting recording. Defaults to `self.stream_resolution`. """ - with self.lock: + with self.lock(timeout=None): # If stream object was destroyed - if not hasattr(self, 'stream'): + if not hasattr(self, "stream"): self.stream = io.BytesIO() # Create a stream object # If no explicit resolution is passed if not resolution: - resolution = self.stream_resolution # Default to video recording resolution + resolution = ( + self.stream_resolution + ) # Default to video recording resolution # Reduce the resolution for video streaming try: self.camera._check_recording_stopped() except picamera.exc.PiCameraRuntimeError: - logging.info("Error while changing resolution: Recording already running.") + logging.info( + "Error while changing resolution: Recording already running." + ) else: self.camera.resolution = resolution # If the stream should be active - if self.state['stream_active']: + if self.stream_active: try: # Start recording on stream port self.camera.start_recording( self.stream, - format='mjpeg', - quality=self.jpeg_quality, - bitrate=-1, # RWB: disable bitrate control + format="mjpeg", + quality=self.mjpeg_quality, + bitrate=-1, # RWB: disable bitrate control # (bitrate control makes JPEG size less good as a focus # metric) - splitter_port=splitter_port) + splitter_port=splitter_port, + ) except picamera.exc.PiCameraAlreadyRecording: - logging.info("Error while starting preview: Recording already running.") + logging.info( + "Error while starting preview: Recording already running." + ) else: - logging.debug("Started MJPEG stream at {} on port {}".format(resolution, splitter_port)) + logging.debug( + "Started MJPEG stream at {} on port {}".format( + resolution, splitter_port + ) + ) def capture( - self, - output, - fmt: str = 'jpeg', - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = True): + self, + output, + fmt: str = "jpeg", + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = True, + ): """ Capture a still image to a StreamObject. @@ -412,34 +513,32 @@ class PiCameraStreamer(BaseCamera): Target object can be overridden for development purposes. Args: - output (CaptureObject/str): Output object to write data bytes to. - use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. + output: String or file-like object to write capture data to fmt (str): Format of the capture. + use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. resize ((int, int)): Resize the captured image. bayer (bool): Store raw bayer data in capture + + Returns: + output_object (str/BytesIO): Target object. """ - with self.lock: - # If output is a StreamObject - if isinstance(output, CaptureObject): - # Set target to capture stream - output_stream = output.stream - else: - output_stream = output - logging.info("Capturing to {}".format(output)) # Set resolution and stop stream recording if necessary if not use_video_port: self.stop_stream_recording() + time.sleep(0.1) self.camera.capture( - output_stream, + output, format=fmt, - quality=100, + quality=self.jpeg_quality, resize=resize, bayer=(not use_video_port) and bayer, - use_video_port=use_video_port) + use_video_port=use_video_port, + ) + #time.sleep(0.1) # Set resolution and start stream recording if necessary if not use_video_port: @@ -448,14 +547,16 @@ class PiCameraStreamer(BaseCamera): return output def yuv( - self, - use_video_port: bool = True, - resize: Tuple[int, int] = None) -> np.ndarray: + self, use_video_port: bool = True, resize: Tuple[int, int] = None + ) -> np.ndarray: """Capture an uncompressed still YUV image to a Numpy array. Args: use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. resize ((int, int)): Resize the captured image. + + Returns: + output_array (np.ndarray): Output array of capture """ with self.lock: if use_video_port: @@ -477,10 +578,8 @@ class PiCameraStreamer(BaseCamera): logging.info("Capturing to {}".format(output)) self.camera.capture( - output, - resize=size, - format='yuv', - use_video_port=use_video_port) + output, resize=size, format="yuv", use_video_port=use_video_port + ) if not use_video_port: self.start_stream_recording() @@ -488,14 +587,16 @@ class PiCameraStreamer(BaseCamera): return output.array def array( - self, - use_video_port: bool = True, - resize: Tuple[int, int] = None) -> np.ndarray: + self, use_video_port: bool = True, resize: Tuple[int, int] = None + ) -> np.ndarray: """Capture an uncompressed still RGB image to a Numpy array. Args: use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. resize ((int, int)): Resize the captured image. + + Returns: + output_array (np.ndarray): Output array of capture """ with self.lock: if use_video_port: @@ -517,10 +618,8 @@ class PiCameraStreamer(BaseCamera): logging.info("Capturing to {}".format(output)) self.camera.capture( - output, - resize=size, - format='rgb', - use_video_port=use_video_port) + output, resize=size, format="rgb", use_video_port=use_video_port + ) # Resume stream self.start_stream_recording() @@ -552,7 +651,6 @@ class PiCameraStreamer(BaseCamera): # Start stream recording (and set resolution) self.start_stream_recording() - # Update state logging.debug("STREAM ACTIVE") # While the iterator is not closed diff --git a/openflexure_microscope/camera/piexif/__init__.py b/openflexure_microscope/camera/piexif/__init__.py index 95cc5de1..95954ecb 100644 --- a/openflexure_microscope/camera/piexif/__init__.py +++ b/openflexure_microscope/camera/piexif/__init__.py @@ -7,5 +7,4 @@ from ._exif import * from ._exceptions import * - -VERSION = '1.1.2' +VERSION = "1.1.2" diff --git a/openflexure_microscope/camera/piexif/_common.py b/openflexure_microscope/camera/piexif/_common.py index 28e45871..2661a1ee 100644 --- a/openflexure_microscope/camera/piexif/_common.py +++ b/openflexure_microscope/camera/piexif/_common.py @@ -12,20 +12,21 @@ def split_into_segments(data): head = 2 segments = [b"\xff\xd8"] while 1: - if data[head: head + 2] == b"\xff\xda": + if data[head : head + 2] == b"\xff\xda": segments.append(data[head:]) break else: - length = struct.unpack(">H", data[head + 2: head + 4])[0] + length = struct.unpack(">H", data[head + 2 : head + 4])[0] endPoint = head + length + 2 - seg = data[head: endPoint] + seg = data[head:endPoint] segments.append(seg) head = endPoint - if (head >= len(data)): + if head >= len(data): raise InvalidImageDataError("Wrong JPEG data.") return segments + def read_exif_from_file(filename): """Slices JPEG meta data into a list from JPEG binary data. """ @@ -39,11 +40,11 @@ def read_exif_from_file(filename): HEAD_LENGTH = 4 exif = None while 1: - length = struct.unpack(">H", head[2: 4])[0] + length = struct.unpack(">H", head[2:4])[0] if head[:2] == b"\xff\xe1": segment_data = f.read(length - 2) - if segment_data[:4] != b'Exif': + if segment_data[:4] != b"Exif": head = f.read(HEAD_LENGTH) continue exif = head + segment_data @@ -57,6 +58,7 @@ def read_exif_from_file(filename): f.close() return exif + def get_exif_seg(segments): """Returns Exif from JPEG meta data list """ @@ -69,9 +71,11 @@ def get_exif_seg(segments): def merge_segments(segments, exif=b""): """Merges Exif with APP0 and APP1 manipulations. """ - if segments[1][0:2] == b"\xff\xe0" and \ - segments[2][0:2] == b"\xff\xe1" and \ - segments[2][4:10] == b"Exif\x00\x00": + if ( + segments[1][0:2] == b"\xff\xe0" + and segments[2][0:2] == b"\xff\xe1" + and segments[2][4:10] == b"Exif\x00\x00" + ): if exif: segments[2] = exif segments.pop(1) @@ -82,8 +86,7 @@ def merge_segments(segments, exif=b""): elif segments[1][0:2] == b"\xff\xe0": if exif: segments[1] = exif - elif segments[1][0:2] == b"\xff\xe1" and \ - segments[1][4:10] == b"Exif\x00\x00": + elif segments[1][0:2] == b"\xff\xe1" and segments[1][4:10] == b"Exif\x00\x00": if exif: segments[1] = exif elif exif is None: diff --git a/openflexure_microscope/camera/piexif/_dump.py b/openflexure_microscope/camera/piexif/_dump.py index 4617a69a..a32f4936 100644 --- a/openflexure_microscope/camera/piexif/_dump.py +++ b/openflexure_microscope/camera/piexif/_dump.py @@ -31,16 +31,20 @@ def dump(exif_dict_original): else: zeroth_ifd = {} - if (("Exif" in exif_dict) and len(exif_dict["Exif"]) or - ("Interop" in exif_dict) and len(exif_dict["Interop"]) ): + if ( + ("Exif" in exif_dict) + and len(exif_dict["Exif"]) + or ("Interop" in exif_dict) + and len(exif_dict["Interop"]) + ): zeroth_ifd[ImageIFD.ExifTag] = 1 exif_is = True exif_ifd = exif_dict["Exif"] if ("Interop" in exif_dict) and len(exif_dict["Interop"]): - exif_ifd[ExifIFD. InteroperabilityTag] = 1 + exif_ifd[ExifIFD.InteroperabilityTag] = 1 interop_is = True interop_ifd = exif_dict["Interop"] - elif ExifIFD. InteroperabilityTag in exif_ifd: + elif ExifIFD.InteroperabilityTag in exif_ifd: exif_ifd.pop(ExifIFD.InteroperabilityTag) elif ImageIFD.ExifTag in zeroth_ifd: zeroth_ifd.pop(ImageIFD.ExifTag) @@ -52,17 +56,20 @@ def dump(exif_dict_original): elif ImageIFD.GPSTag in zeroth_ifd: zeroth_ifd.pop(ImageIFD.GPSTag) - if (("1st" in exif_dict) and - ("thumbnail" in exif_dict) and - (exif_dict["thumbnail"] is not None)): + if ( + ("1st" in exif_dict) + and ("thumbnail" in exif_dict) + and (exif_dict["thumbnail"] is not None) + ): first_is = True exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1 exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1 first_ifd = exif_dict["1st"] zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0) - zeroth_length = (len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + - len(zeroth_set[1])) + zeroth_length = ( + len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + len(zeroth_set[1]) + ) if exif_is: exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length) @@ -115,8 +122,7 @@ def dump(exif_dict_original): else: gps_pointer = b"" if interop_is: - pointer_value = (TIFF_HEADER_LENGTH + - zeroth_length + exif_length + gps_length) + pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length pointer_str = struct.pack(">I", pointer_value) key = ExifIFD.InteroperabilityTag key_str = struct.pack(">H", key) @@ -126,33 +132,46 @@ def dump(exif_dict_original): else: interop_pointer = b"" if first_is: - pointer_value = (TIFF_HEADER_LENGTH + zeroth_length + - exif_length + gps_length + interop_length) + pointer_value = ( + TIFF_HEADER_LENGTH + + zeroth_length + + exif_length + + gps_length + + interop_length + ) first_ifd_pointer = struct.pack(">L", pointer_value) - thumbnail_pointer = (pointer_value + len(first_set[0]) + 24 + - 4 + len(first_set[1])) - thumbnail_p_bytes = (b"\x02\x01\x00\x04\x00\x00\x00\x01" + - struct.pack(">L", thumbnail_pointer)) - thumbnail_length_bytes = (b"\x02\x02\x00\x04\x00\x00\x00\x01" + - struct.pack(">L", len(thumbnail))) - first_bytes = (first_set[0] + thumbnail_p_bytes + - thumbnail_length_bytes + b"\x00\x00\x00\x00" + - first_set[1] + thumbnail) + thumbnail_pointer = ( + pointer_value + len(first_set[0]) + 24 + 4 + len(first_set[1]) + ) + thumbnail_p_bytes = b"\x02\x01\x00\x04\x00\x00\x00\x01" + struct.pack( + ">L", thumbnail_pointer + ) + thumbnail_length_bytes = b"\x02\x02\x00\x04\x00\x00\x00\x01" + struct.pack( + ">L", len(thumbnail) + ) + first_bytes = ( + first_set[0] + + thumbnail_p_bytes + + thumbnail_length_bytes + + b"\x00\x00\x00\x00" + + first_set[1] + + thumbnail + ) else: first_ifd_pointer = b"\x00\x00\x00\x00" - zeroth_bytes = (zeroth_set[0] + exif_pointer + gps_pointer + - first_ifd_pointer + zeroth_set[1]) + zeroth_bytes = ( + zeroth_set[0] + exif_pointer + gps_pointer + first_ifd_pointer + zeroth_set[1] + ) if exif_is: exif_bytes = exif_set[0] + interop_pointer + exif_set[1] - return (header + zeroth_bytes + exif_bytes + gps_bytes + - interop_bytes + first_bytes) + return header + zeroth_bytes + exif_bytes + gps_bytes + interop_bytes + first_bytes def _get_thumbnail(jpeg): segments = split_into_segments(jpeg) - while (b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef"): + while b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef": segments.pop(1) thumbnail = b"".join(segments) return thumbnail @@ -161,24 +180,31 @@ def _get_thumbnail(jpeg): def _pack_byte(*args): return struct.pack("B" * len(args), *args) + def _pack_signed_byte(*args): return struct.pack("b" * len(args), *args) + def _pack_short(*args): return struct.pack(">" + "H" * len(args), *args) + def _pack_signed_short(*args): return struct.pack(">" + "h" * len(args), *args) + def _pack_long(*args): return struct.pack(">" + "L" * len(args), *args) + def _pack_slong(*args): return struct.pack(">" + "l" * len(args), *args) + def _pack_float(*args): return struct.pack(">" + "f" * len(args), *args) + def _pack_double(*args): return struct.pack(">" + "d" * len(args), *args) @@ -190,16 +216,14 @@ def _value_to_bytes(raw_value, value_type, offset): if value_type == TYPES.Byte: length = len(raw_value) if length <= 4: - value_str = (_pack_byte(*raw_value) + - b"\x00" * (4 - length)) + value_str = _pack_byte(*raw_value) + b"\x00" * (4 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_byte(*raw_value) elif value_type == TYPES.Short: length = len(raw_value) if length <= 2: - value_str = (_pack_short(*raw_value) + - b"\x00\x00" * (2 - length)) + value_str = _pack_short(*raw_value) + b"\x00\x00" * (2 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_short(*raw_value) @@ -241,8 +265,7 @@ def _value_to_bytes(raw_value, value_type, offset): new_value = b"" for n, val in enumerate(raw_value): num, den = val - new_value += (struct.pack(">L", num) + - struct.pack(">L", den)) + new_value += struct.pack(">L", num) + struct.pack(">L", den) value_str = struct.pack(">I", offset) four_bytes_over = new_value elif value_type == TYPES.SRational: @@ -255,8 +278,7 @@ def _value_to_bytes(raw_value, value_type, offset): new_value = b"" for n, val in enumerate(raw_value): num, den = val - new_value += (struct.pack(">l", num) + - struct.pack(">l", den)) + new_value += struct.pack(">l", num) + struct.pack(">l", den) value_str = struct.pack(">I", offset) four_bytes_over = new_value elif value_type == TYPES.Undefined: @@ -272,19 +294,17 @@ def _value_to_bytes(raw_value, value_type, offset): value_str = raw_value + b"\x00" * (4 - length) except TypeError: raise ValueError("Got invalid type to convert.") - elif value_type == TYPES.SByte: # Signed Byte + elif value_type == TYPES.SByte: # Signed Byte length = len(raw_value) if length <= 4: - value_str = (_pack_signed_byte(*raw_value) + - b"\x00" * (4 - length)) + value_str = _pack_signed_byte(*raw_value) + b"\x00" * (4 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_signed_byte(*raw_value) - elif value_type == TYPES.SShort: # Signed Short + elif value_type == TYPES.SShort: # Signed Short length = len(raw_value) if length <= 2: - value_str = (_pack_signed_short(*raw_value) + - b"\x00\x00" * (2 - length)) + value_str = _pack_signed_short(*raw_value) + b"\x00\x00" * (2 - length) else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_signed_short(*raw_value) @@ -295,7 +315,7 @@ def _value_to_bytes(raw_value, value_type, offset): else: value_str = struct.pack(">I", offset) four_bytes_over = _pack_float(*raw_value) - elif value_type == TYPES.DFloat: # Double + elif value_type == TYPES.DFloat: # Double length = len(raw_value) value_str = struct.pack(">I", offset) four_bytes_over = _pack_double(*raw_value) @@ -303,6 +323,7 @@ def _value_to_bytes(raw_value, value_type, offset): length_str = struct.pack(">I", length) return length_str, value_str, four_bytes_over + def _dict_to_bytes(ifd_dict, ifd, ifd_offset): tag_count = len(ifd_dict) entry_header = struct.pack(">H", tag_count) @@ -318,7 +339,10 @@ def _dict_to_bytes(ifd_dict, ifd, ifd_offset): continue elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag): continue - elif (ifd == "1st") and (key in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)): + elif (ifd == "1st") and ( + key + in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength) + ): continue raw_value = ifd_dict[key] @@ -332,13 +356,13 @@ def _dict_to_bytes(ifd_dict, ifd, ifd_offset): offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + len(values) try: - length_str, value_str, four_bytes_over = _value_to_bytes(raw_value, - value_type, - offset) + length_str, value_str, four_bytes_over = _value_to_bytes( + raw_value, value_type, offset + ) except ValueError: raise ValueError( - '"dump" got wrong type of exif value.\n' + - '{0} in {1} IFD. Got as {2}.'.format(key, ifd, type(ifd_dict[key])) + '"dump" got wrong type of exif value.\n' + + "{0} in {1} IFD. Got as {2}.".format(key, ifd, type(ifd_dict[key])) ) entries += key_str + type_str + length_str + value_str diff --git a/openflexure_microscope/camera/piexif/_exif.py b/openflexure_microscope/camera/piexif/_exif.py index abdf0bc4..3c25003c 100644 --- a/openflexure_microscope/camera/piexif/_exif.py +++ b/openflexure_microscope/camera/piexif/_exif.py @@ -14,314 +14,322 @@ class TYPES: TAGS = { - 'Image': {11: {'name': 'ProcessingSoftware', 'type': TYPES.Ascii}, - 254: {'name': 'NewSubfileType', 'type': TYPES.Long}, - 255: {'name': 'SubfileType', 'type': TYPES.Short}, - 256: {'name': 'ImageWidth', 'type': TYPES.Long}, - 257: {'name': 'ImageLength', 'type': TYPES.Long}, - 258: {'name': 'BitsPerSample', 'type': TYPES.Short}, - 259: {'name': 'Compression', 'type': TYPES.Short}, - 262: {'name': 'PhotometricInterpretation', 'type': TYPES.Short}, - 263: {'name': 'Threshholding', 'type': TYPES.Short}, - 264: {'name': 'CellWidth', 'type': TYPES.Short}, - 265: {'name': 'CellLength', 'type': TYPES.Short}, - 266: {'name': 'FillOrder', 'type': TYPES.Short}, - 269: {'name': 'DocumentName', 'type': TYPES.Ascii}, - 270: {'name': 'ImageDescription', 'type': TYPES.Ascii}, - 271: {'name': 'Make', 'type': TYPES.Ascii}, - 272: {'name': 'Model', 'type': TYPES.Ascii}, - 273: {'name': 'StripOffsets', 'type': TYPES.Long}, - 274: {'name': 'Orientation', 'type': TYPES.Short}, - 277: {'name': 'SamplesPerPixel', 'type': TYPES.Short}, - 278: {'name': 'RowsPerStrip', 'type': TYPES.Long}, - 279: {'name': 'StripByteCounts', 'type': TYPES.Long}, - 282: {'name': 'XResolution', 'type': TYPES.Rational}, - 283: {'name': 'YResolution', 'type': TYPES.Rational}, - 284: {'name': 'PlanarConfiguration', 'type': TYPES.Short}, - 290: {'name': 'GrayResponseUnit', 'type': TYPES.Short}, - 291: {'name': 'GrayResponseCurve', 'type': TYPES.Short}, - 292: {'name': 'T4Options', 'type': TYPES.Long}, - 293: {'name': 'T6Options', 'type': TYPES.Long}, - 296: {'name': 'ResolutionUnit', 'type': TYPES.Short}, - 301: {'name': 'TransferFunction', 'type': TYPES.Short}, - 305: {'name': 'Software', 'type': TYPES.Ascii}, - 306: {'name': 'DateTime', 'type': TYPES.Ascii}, - 315: {'name': 'Artist', 'type': TYPES.Ascii}, - 316: {'name': 'HostComputer', 'type': TYPES.Ascii}, - 317: {'name': 'Predictor', 'type': TYPES.Short}, - 318: {'name': 'WhitePoint', 'type': TYPES.Rational}, - 319: {'name': 'PrimaryChromaticities', 'type': TYPES.Rational}, - 320: {'name': 'ColorMap', 'type': TYPES.Short}, - 321: {'name': 'HalftoneHints', 'type': TYPES.Short}, - 322: {'name': 'TileWidth', 'type': TYPES.Short}, - 323: {'name': 'TileLength', 'type': TYPES.Short}, - 324: {'name': 'TileOffsets', 'type': TYPES.Short}, - 325: {'name': 'TileByteCounts', 'type': TYPES.Short}, - 330: {'name': 'SubIFDs', 'type': TYPES.Long}, - 332: {'name': 'InkSet', 'type': TYPES.Short}, - 333: {'name': 'InkNames', 'type': TYPES.Ascii}, - 334: {'name': 'NumberOfInks', 'type': TYPES.Short}, - 336: {'name': 'DotRange', 'type': TYPES.Byte}, - 337: {'name': 'TargetPrinter', 'type': TYPES.Ascii}, - 338: {'name': 'ExtraSamples', 'type': TYPES.Short}, - 339: {'name': 'SampleFormat', 'type': TYPES.Short}, - 340: {'name': 'SMinSampleValue', 'type': TYPES.Short}, - 341: {'name': 'SMaxSampleValue', 'type': TYPES.Short}, - 342: {'name': 'TransferRange', 'type': TYPES.Short}, - 343: {'name': 'ClipPath', 'type': TYPES.Byte}, - 344: {'name': 'XClipPathUnits', 'type': TYPES.Long}, - 345: {'name': 'YClipPathUnits', 'type': TYPES.Long}, - 346: {'name': 'Indexed', 'type': TYPES.Short}, - 347: {'name': 'JPEGTables', 'type': TYPES.Undefined}, - 351: {'name': 'OPIProxy', 'type': TYPES.Short}, - 512: {'name': 'JPEGProc', 'type': TYPES.Long}, - 513: {'name': 'JPEGInterchangeFormat', 'type': TYPES.Long}, - 514: {'name': 'JPEGInterchangeFormatLength', 'type': TYPES.Long}, - 515: {'name': 'JPEGRestartInterval', 'type': TYPES.Short}, - 517: {'name': 'JPEGLosslessPredictors', 'type': TYPES.Short}, - 518: {'name': 'JPEGPointTransforms', 'type': TYPES.Short}, - 519: {'name': 'JPEGQTables', 'type': TYPES.Long}, - 520: {'name': 'JPEGDCTables', 'type': TYPES.Long}, - 521: {'name': 'JPEGACTables', 'type': TYPES.Long}, - 529: {'name': 'YCbCrCoefficients', 'type': TYPES.Rational}, - 530: {'name': 'YCbCrSubSampling', 'type': TYPES.Short}, - 531: {'name': 'YCbCrPositioning', 'type': TYPES.Short}, - 532: {'name': 'ReferenceBlackWhite', 'type': TYPES.Rational}, - 700: {'name': 'XMLPacket', 'type': TYPES.Byte}, - 18246: {'name': 'Rating', 'type': TYPES.Short}, - 18249: {'name': 'RatingPercent', 'type': TYPES.Short}, - 32781: {'name': 'ImageID', 'type': TYPES.Ascii}, - 33421: {'name': 'CFARepeatPatternDim', 'type': TYPES.Short}, - 33422: {'name': 'CFAPattern', 'type': TYPES.Byte}, - 33423: {'name': 'BatteryLevel', 'type': TYPES.Rational}, - 33432: {'name': 'Copyright', 'type': TYPES.Ascii}, - 33434: {'name': 'ExposureTime', 'type': TYPES.Rational}, - 34377: {'name': 'ImageResources', 'type': TYPES.Byte}, - 34665: {'name': 'ExifTag', 'type': TYPES.Long}, - 34675: {'name': 'InterColorProfile', 'type': TYPES.Undefined}, - 34853: {'name': 'GPSTag', 'type': TYPES.Long}, - 34857: {'name': 'Interlace', 'type': TYPES.Short}, - 34858: {'name': 'TimeZoneOffset', 'type': TYPES.Long}, - 34859: {'name': 'SelfTimerMode', 'type': TYPES.Short}, - 37387: {'name': 'FlashEnergy', 'type': TYPES.Rational}, - 37388: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined}, - 37389: {'name': 'Noise', 'type': TYPES.Undefined}, - 37390: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational}, - 37391: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational}, - 37392: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short}, - 37393: {'name': 'ImageNumber', 'type': TYPES.Long}, - 37394: {'name': 'SecurityClassification', 'type': TYPES.Ascii}, - 37395: {'name': 'ImageHistory', 'type': TYPES.Ascii}, - 37397: {'name': 'ExposureIndex', 'type': TYPES.Rational}, - 37398: {'name': 'TIFFEPStandardID', 'type': TYPES.Byte}, - 37399: {'name': 'SensingMethod', 'type': TYPES.Short}, - 40091: {'name': 'XPTitle', 'type': TYPES.Byte}, - 40092: {'name': 'XPComment', 'type': TYPES.Byte}, - 40093: {'name': 'XPAuthor', 'type': TYPES.Byte}, - 40094: {'name': 'XPKeywords', 'type': TYPES.Byte}, - 40095: {'name': 'XPSubject', 'type': TYPES.Byte}, - 50341: {'name': 'PrintImageMatching', 'type': TYPES.Undefined}, - 50706: {'name': 'DNGVersion', 'type': TYPES.Byte}, - 50707: {'name': 'DNGBackwardVersion', 'type': TYPES.Byte}, - 50708: {'name': 'UniqueCameraModel', 'type': TYPES.Ascii}, - 50709: {'name': 'LocalizedCameraModel', 'type': TYPES.Byte}, - 50710: {'name': 'CFAPlaneColor', 'type': TYPES.Byte}, - 50711: {'name': 'CFALayout', 'type': TYPES.Short}, - 50712: {'name': 'LinearizationTable', 'type': TYPES.Short}, - 50713: {'name': 'BlackLevelRepeatDim', 'type': TYPES.Short}, - 50714: {'name': 'BlackLevel', 'type': TYPES.Rational}, - 50715: {'name': 'BlackLevelDeltaH', 'type': TYPES.SRational}, - 50716: {'name': 'BlackLevelDeltaV', 'type': TYPES.SRational}, - 50717: {'name': 'WhiteLevel', 'type': TYPES.Short}, - 50718: {'name': 'DefaultScale', 'type': TYPES.Rational}, - 50719: {'name': 'DefaultCropOrigin', 'type': TYPES.Short}, - 50720: {'name': 'DefaultCropSize', 'type': TYPES.Short}, - 50721: {'name': 'ColorMatrix1', 'type': TYPES.SRational}, - 50722: {'name': 'ColorMatrix2', 'type': TYPES.SRational}, - 50723: {'name': 'CameraCalibration1', 'type': TYPES.SRational}, - 50724: {'name': 'CameraCalibration2', 'type': TYPES.SRational}, - 50725: {'name': 'ReductionMatrix1', 'type': TYPES.SRational}, - 50726: {'name': 'ReductionMatrix2', 'type': TYPES.SRational}, - 50727: {'name': 'AnalogBalance', 'type': TYPES.Rational}, - 50728: {'name': 'AsShotNeutral', 'type': TYPES.Short}, - 50729: {'name': 'AsShotWhiteXY', 'type': TYPES.Rational}, - 50730: {'name': 'BaselineExposure', 'type': TYPES.SRational}, - 50731: {'name': 'BaselineNoise', 'type': TYPES.Rational}, - 50732: {'name': 'BaselineSharpness', 'type': TYPES.Rational}, - 50733: {'name': 'BayerGreenSplit', 'type': TYPES.Long}, - 50734: {'name': 'LinearResponseLimit', 'type': TYPES.Rational}, - 50735: {'name': 'CameraSerialNumber', 'type': TYPES.Ascii}, - 50736: {'name': 'LensInfo', 'type': TYPES.Rational}, - 50737: {'name': 'ChromaBlurRadius', 'type': TYPES.Rational}, - 50738: {'name': 'AntiAliasStrength', 'type': TYPES.Rational}, - 50739: {'name': 'ShadowScale', 'type': TYPES.SRational}, - 50740: {'name': 'DNGPrivateData', 'type': TYPES.Byte}, - 50741: {'name': 'MakerNoteSafety', 'type': TYPES.Short}, - 50778: {'name': 'CalibrationIlluminant1', 'type': TYPES.Short}, - 50779: {'name': 'CalibrationIlluminant2', 'type': TYPES.Short}, - 50780: {'name': 'BestQualityScale', 'type': TYPES.Rational}, - 50781: {'name': 'RawDataUniqueID', 'type': TYPES.Byte}, - 50827: {'name': 'OriginalRawFileName', 'type': TYPES.Byte}, - 50828: {'name': 'OriginalRawFileData', 'type': TYPES.Undefined}, - 50829: {'name': 'ActiveArea', 'type': TYPES.Short}, - 50830: {'name': 'MaskedAreas', 'type': TYPES.Short}, - 50831: {'name': 'AsShotICCProfile', 'type': TYPES.Undefined}, - 50832: {'name': 'AsShotPreProfileMatrix', 'type': TYPES.SRational}, - 50833: {'name': 'CurrentICCProfile', 'type': TYPES.Undefined}, - 50834: {'name': 'CurrentPreProfileMatrix', 'type': TYPES.SRational}, - 50879: {'name': 'ColorimetricReference', 'type': TYPES.Short}, - 50931: {'name': 'CameraCalibrationSignature', 'type': TYPES.Byte}, - 50932: {'name': 'ProfileCalibrationSignature', 'type': TYPES.Byte}, - 50934: {'name': 'AsShotProfileName', 'type': TYPES.Byte}, - 50935: {'name': 'NoiseReductionApplied', 'type': TYPES.Rational}, - 50936: {'name': 'ProfileName', 'type': TYPES.Byte}, - 50937: {'name': 'ProfileHueSatMapDims', 'type': TYPES.Long}, - 50938: {'name': 'ProfileHueSatMapData1', 'type': TYPES.Float}, - 50939: {'name': 'ProfileHueSatMapData2', 'type': TYPES.Float}, - 50940: {'name': 'ProfileToneCurve', 'type': TYPES.Float}, - 50941: {'name': 'ProfileEmbedPolicy', 'type': TYPES.Long}, - 50942: {'name': 'ProfileCopyright', 'type': TYPES.Byte}, - 50964: {'name': 'ForwardMatrix1', 'type': TYPES.SRational}, - 50965: {'name': 'ForwardMatrix2', 'type': TYPES.SRational}, - 50966: {'name': 'PreviewApplicationName', 'type': TYPES.Byte}, - 50967: {'name': 'PreviewApplicationVersion', 'type': TYPES.Byte}, - 50968: {'name': 'PreviewSettingsName', 'type': TYPES.Byte}, - 50969: {'name': 'PreviewSettingsDigest', 'type': TYPES.Byte}, - 50970: {'name': 'PreviewColorSpace', 'type': TYPES.Long}, - 50971: {'name': 'PreviewDateTime', 'type': TYPES.Ascii}, - 50972: {'name': 'RawImageDigest', 'type': TYPES.Undefined}, - 50973: {'name': 'OriginalRawFileDigest', 'type': TYPES.Undefined}, - 50974: {'name': 'SubTileBlockSize', 'type': TYPES.Long}, - 50975: {'name': 'RowInterleaveFactor', 'type': TYPES.Long}, - 50981: {'name': 'ProfileLookTableDims', 'type': TYPES.Long}, - 50982: {'name': 'ProfileLookTableData', 'type': TYPES.Float}, - 51008: {'name': 'OpcodeList1', 'type': TYPES.Undefined}, - 51009: {'name': 'OpcodeList2', 'type': TYPES.Undefined}, - 51022: {'name': 'OpcodeList3', 'type': TYPES.Undefined}, - 60606: {'name': 'ZZZTestSlong1', 'type': TYPES.SLong}, - 60607: {'name': 'ZZZTestSlong2', 'type': TYPES.SLong}, - 60608: {'name': 'ZZZTestSByte', 'type': TYPES.SByte}, - 60609: {'name': 'ZZZTestSShort', 'type': TYPES.SShort}, - 60610: {'name': 'ZZZTestDFloat', 'type': TYPES.DFloat},}, - 'Exif': {33434: {'name': 'ExposureTime', 'type': TYPES.Rational}, - 33437: {'name': 'FNumber', 'type': TYPES.Rational}, - 34850: {'name': 'ExposureProgram', 'type': TYPES.Short}, - 34852: {'name': 'SpectralSensitivity', 'type': TYPES.Ascii}, - 34855: {'name': 'ISOSpeedRatings', 'type': TYPES.Short}, - 34856: {'name': 'OECF', 'type': TYPES.Undefined}, - 34864: {'name': 'SensitivityType', 'type': TYPES.Short}, - 34865: {'name': 'StandardOutputSensitivity', 'type': TYPES.Long}, - 34866: {'name': 'RecommendedExposureIndex', 'type': TYPES.Long}, - 34867: {'name': 'ISOSpeed', 'type': TYPES.Long}, - 34868: {'name': 'ISOSpeedLatitudeyyy', 'type': TYPES.Long}, - 34869: {'name': 'ISOSpeedLatitudezzz', 'type': TYPES.Long}, - 36864: {'name': 'ExifVersion', 'type': TYPES.Undefined}, - 36867: {'name': 'DateTimeOriginal', 'type': TYPES.Ascii}, - 36868: {'name': 'DateTimeDigitized', 'type': TYPES.Ascii}, - 36880: {'name': 'OffsetTime', 'type': TYPES.Ascii}, - 36881: {'name': 'OffsetTimeOriginal', 'type': TYPES.Ascii}, - 36882: {'name': 'OffsetTimeDigitized', 'type': TYPES.Ascii}, - 37121: {'name': 'ComponentsConfiguration', 'type': TYPES.Undefined}, - 37122: {'name': 'CompressedBitsPerPixel', 'type': TYPES.Rational}, - 37377: {'name': 'ShutterSpeedValue', 'type': TYPES.SRational}, - 37378: {'name': 'ApertureValue', 'type': TYPES.Rational}, - 37379: {'name': 'BrightnessValue', 'type': TYPES.SRational}, - 37380: {'name': 'ExposureBiasValue', 'type': TYPES.SRational}, - 37381: {'name': 'MaxApertureValue', 'type': TYPES.Rational}, - 37382: {'name': 'SubjectDistance', 'type': TYPES.Rational}, - 37383: {'name': 'MeteringMode', 'type': TYPES.Short}, - 37384: {'name': 'LightSource', 'type': TYPES.Short}, - 37385: {'name': 'Flash', 'type': TYPES.Short}, - 37386: {'name': 'FocalLength', 'type': TYPES.Rational}, - 37396: {'name': 'SubjectArea', 'type': TYPES.Short}, - 37500: {'name': 'MakerNote', 'type': TYPES.Undefined}, - 37510: {'name': 'UserComment', 'type': TYPES.Undefined}, - 37520: {'name': 'SubSecTime', 'type': TYPES.Ascii}, - 37521: {'name': 'SubSecTimeOriginal', 'type': TYPES.Ascii}, - 37522: {'name': 'SubSecTimeDigitized', 'type': TYPES.Ascii}, - 37888: {'name': 'Temperature', 'type': TYPES.SRational}, - 37889: {'name': 'Humidity', 'type': TYPES.Rational}, - 37890: {'name': 'Pressure', 'type': TYPES.Rational}, - 37891: {'name': 'WaterDepth', 'type': TYPES.SRational}, - 37892: {'name': 'Acceleration', 'type': TYPES.Rational}, - 37893: {'name': 'CameraElevationAngle', 'type': TYPES.SRational}, - 40960: {'name': 'FlashpixVersion', 'type': TYPES.Undefined}, - 40961: {'name': 'ColorSpace', 'type': TYPES.Short}, - 40962: {'name': 'PixelXDimension', 'type': TYPES.Long}, - 40963: {'name': 'PixelYDimension', 'type': TYPES.Long}, - 40964: {'name': 'RelatedSoundFile', 'type': TYPES.Ascii}, - 40965: {'name': 'InteroperabilityTag', 'type': TYPES.Long}, - 41483: {'name': 'FlashEnergy', 'type': TYPES.Rational}, - 41484: {'name': 'SpatialFrequencyResponse', 'type': TYPES.Undefined}, - 41486: {'name': 'FocalPlaneXResolution', 'type': TYPES.Rational}, - 41487: {'name': 'FocalPlaneYResolution', 'type': TYPES.Rational}, - 41488: {'name': 'FocalPlaneResolutionUnit', 'type': TYPES.Short}, - 41492: {'name': 'SubjectLocation', 'type': TYPES.Short}, - 41493: {'name': 'ExposureIndex', 'type': TYPES.Rational}, - 41495: {'name': 'SensingMethod', 'type': TYPES.Short}, - 41728: {'name': 'FileSource', 'type': TYPES.Undefined}, - 41729: {'name': 'SceneType', 'type': TYPES.Undefined}, - 41730: {'name': 'CFAPattern', 'type': TYPES.Undefined}, - 41985: {'name': 'CustomRendered', 'type': TYPES.Short}, - 41986: {'name': 'ExposureMode', 'type': TYPES.Short}, - 41987: {'name': 'WhiteBalance', 'type': TYPES.Short}, - 41988: {'name': 'DigitalZoomRatio', 'type': TYPES.Rational}, - 41989: {'name': 'FocalLengthIn35mmFilm', 'type': TYPES.Short}, - 41990: {'name': 'SceneCaptureType', 'type': TYPES.Short}, - 41991: {'name': 'GainControl', 'type': TYPES.Short}, - 41992: {'name': 'Contrast', 'type': TYPES.Short}, - 41993: {'name': 'Saturation', 'type': TYPES.Short}, - 41994: {'name': 'Sharpness', 'type': TYPES.Short}, - 41995: {'name': 'DeviceSettingDescription', 'type': TYPES.Undefined}, - 41996: {'name': 'SubjectDistanceRange', 'type': TYPES.Short}, - 42016: {'name': 'ImageUniqueID', 'type': TYPES.Ascii}, - 42032: {'name': 'CameraOwnerName', 'type': TYPES.Ascii}, - 42033: {'name': 'BodySerialNumber', 'type': TYPES.Ascii}, - 42034: {'name': 'LensSpecification', 'type': TYPES.Rational}, - 42035: {'name': 'LensMake', 'type': TYPES.Ascii}, - 42036: {'name': 'LensModel', 'type': TYPES.Ascii}, - 42037: {'name': 'LensSerialNumber', 'type': TYPES.Ascii}, - 42240: {'name': 'Gamma', 'type': TYPES.Rational}}, - 'GPS': {0: {'name': 'GPSVersionID', 'type': TYPES.Byte}, - 1: {'name': 'GPSLatitudeRef', 'type': TYPES.Ascii}, - 2: {'name': 'GPSLatitude', 'type': TYPES.Rational}, - 3: {'name': 'GPSLongitudeRef', 'type': TYPES.Ascii}, - 4: {'name': 'GPSLongitude', 'type': TYPES.Rational}, - 5: {'name': 'GPSAltitudeRef', 'type': TYPES.Byte}, - 6: {'name': 'GPSAltitude', 'type': TYPES.Rational}, - 7: {'name': 'GPSTimeStamp', 'type': TYPES.Rational}, - 8: {'name': 'GPSSatellites', 'type': TYPES.Ascii}, - 9: {'name': 'GPSStatus', 'type': TYPES.Ascii}, - 10: {'name': 'GPSMeasureMode', 'type': TYPES.Ascii}, - 11: {'name': 'GPSDOP', 'type': TYPES.Rational}, - 12: {'name': 'GPSSpeedRef', 'type': TYPES.Ascii}, - 13: {'name': 'GPSSpeed', 'type': TYPES.Rational}, - 14: {'name': 'GPSTrackRef', 'type': TYPES.Ascii}, - 15: {'name': 'GPSTrack', 'type': TYPES.Rational}, - 16: {'name': 'GPSImgDirectionRef', 'type': TYPES.Ascii}, - 17: {'name': 'GPSImgDirection', 'type': TYPES.Rational}, - 18: {'name': 'GPSMapDatum', 'type': TYPES.Ascii}, - 19: {'name': 'GPSDestLatitudeRef', 'type': TYPES.Ascii}, - 20: {'name': 'GPSDestLatitude', 'type': TYPES.Rational}, - 21: {'name': 'GPSDestLongitudeRef', 'type': TYPES.Ascii}, - 22: {'name': 'GPSDestLongitude', 'type': TYPES.Rational}, - 23: {'name': 'GPSDestBearingRef', 'type': TYPES.Ascii}, - 24: {'name': 'GPSDestBearing', 'type': TYPES.Rational}, - 25: {'name': 'GPSDestDistanceRef', 'type': TYPES.Ascii}, - 26: {'name': 'GPSDestDistance', 'type': TYPES.Rational}, - 27: {'name': 'GPSProcessingMethod', 'type': TYPES.Undefined}, - 28: {'name': 'GPSAreaInformation', 'type': TYPES.Undefined}, - 29: {'name': 'GPSDateStamp', 'type': TYPES.Ascii}, - 30: {'name': 'GPSDifferential', 'type': TYPES.Short}, - 31: {'name': 'GPSHPositioningError', 'type': TYPES.Rational}}, - 'Interop': {1: {'name': 'InteroperabilityIndex', 'type': TYPES.Ascii}}, + "Image": { + 11: {"name": "ProcessingSoftware", "type": TYPES.Ascii}, + 254: {"name": "NewSubfileType", "type": TYPES.Long}, + 255: {"name": "SubfileType", "type": TYPES.Short}, + 256: {"name": "ImageWidth", "type": TYPES.Long}, + 257: {"name": "ImageLength", "type": TYPES.Long}, + 258: {"name": "BitsPerSample", "type": TYPES.Short}, + 259: {"name": "Compression", "type": TYPES.Short}, + 262: {"name": "PhotometricInterpretation", "type": TYPES.Short}, + 263: {"name": "Threshholding", "type": TYPES.Short}, + 264: {"name": "CellWidth", "type": TYPES.Short}, + 265: {"name": "CellLength", "type": TYPES.Short}, + 266: {"name": "FillOrder", "type": TYPES.Short}, + 269: {"name": "DocumentName", "type": TYPES.Ascii}, + 270: {"name": "ImageDescription", "type": TYPES.Ascii}, + 271: {"name": "Make", "type": TYPES.Ascii}, + 272: {"name": "Model", "type": TYPES.Ascii}, + 273: {"name": "StripOffsets", "type": TYPES.Long}, + 274: {"name": "Orientation", "type": TYPES.Short}, + 277: {"name": "SamplesPerPixel", "type": TYPES.Short}, + 278: {"name": "RowsPerStrip", "type": TYPES.Long}, + 279: {"name": "StripByteCounts", "type": TYPES.Long}, + 282: {"name": "XResolution", "type": TYPES.Rational}, + 283: {"name": "YResolution", "type": TYPES.Rational}, + 284: {"name": "PlanarConfiguration", "type": TYPES.Short}, + 290: {"name": "GrayResponseUnit", "type": TYPES.Short}, + 291: {"name": "GrayResponseCurve", "type": TYPES.Short}, + 292: {"name": "T4Options", "type": TYPES.Long}, + 293: {"name": "T6Options", "type": TYPES.Long}, + 296: {"name": "ResolutionUnit", "type": TYPES.Short}, + 301: {"name": "TransferFunction", "type": TYPES.Short}, + 305: {"name": "Software", "type": TYPES.Ascii}, + 306: {"name": "DateTime", "type": TYPES.Ascii}, + 315: {"name": "Artist", "type": TYPES.Ascii}, + 316: {"name": "HostComputer", "type": TYPES.Ascii}, + 317: {"name": "Predictor", "type": TYPES.Short}, + 318: {"name": "WhitePoint", "type": TYPES.Rational}, + 319: {"name": "PrimaryChromaticities", "type": TYPES.Rational}, + 320: {"name": "ColorMap", "type": TYPES.Short}, + 321: {"name": "HalftoneHints", "type": TYPES.Short}, + 322: {"name": "TileWidth", "type": TYPES.Short}, + 323: {"name": "TileLength", "type": TYPES.Short}, + 324: {"name": "TileOffsets", "type": TYPES.Short}, + 325: {"name": "TileByteCounts", "type": TYPES.Short}, + 330: {"name": "SubIFDs", "type": TYPES.Long}, + 332: {"name": "InkSet", "type": TYPES.Short}, + 333: {"name": "InkNames", "type": TYPES.Ascii}, + 334: {"name": "NumberOfInks", "type": TYPES.Short}, + 336: {"name": "DotRange", "type": TYPES.Byte}, + 337: {"name": "TargetPrinter", "type": TYPES.Ascii}, + 338: {"name": "ExtraSamples", "type": TYPES.Short}, + 339: {"name": "SampleFormat", "type": TYPES.Short}, + 340: {"name": "SMinSampleValue", "type": TYPES.Short}, + 341: {"name": "SMaxSampleValue", "type": TYPES.Short}, + 342: {"name": "TransferRange", "type": TYPES.Short}, + 343: {"name": "ClipPath", "type": TYPES.Byte}, + 344: {"name": "XClipPathUnits", "type": TYPES.Long}, + 345: {"name": "YClipPathUnits", "type": TYPES.Long}, + 346: {"name": "Indexed", "type": TYPES.Short}, + 347: {"name": "JPEGTables", "type": TYPES.Undefined}, + 351: {"name": "OPIProxy", "type": TYPES.Short}, + 512: {"name": "JPEGProc", "type": TYPES.Long}, + 513: {"name": "JPEGInterchangeFormat", "type": TYPES.Long}, + 514: {"name": "JPEGInterchangeFormatLength", "type": TYPES.Long}, + 515: {"name": "JPEGRestartInterval", "type": TYPES.Short}, + 517: {"name": "JPEGLosslessPredictors", "type": TYPES.Short}, + 518: {"name": "JPEGPointTransforms", "type": TYPES.Short}, + 519: {"name": "JPEGQTables", "type": TYPES.Long}, + 520: {"name": "JPEGDCTables", "type": TYPES.Long}, + 521: {"name": "JPEGACTables", "type": TYPES.Long}, + 529: {"name": "YCbCrCoefficients", "type": TYPES.Rational}, + 530: {"name": "YCbCrSubSampling", "type": TYPES.Short}, + 531: {"name": "YCbCrPositioning", "type": TYPES.Short}, + 532: {"name": "ReferenceBlackWhite", "type": TYPES.Rational}, + 700: {"name": "XMLPacket", "type": TYPES.Byte}, + 18246: {"name": "Rating", "type": TYPES.Short}, + 18249: {"name": "RatingPercent", "type": TYPES.Short}, + 32781: {"name": "ImageID", "type": TYPES.Ascii}, + 33421: {"name": "CFARepeatPatternDim", "type": TYPES.Short}, + 33422: {"name": "CFAPattern", "type": TYPES.Byte}, + 33423: {"name": "BatteryLevel", "type": TYPES.Rational}, + 33432: {"name": "Copyright", "type": TYPES.Ascii}, + 33434: {"name": "ExposureTime", "type": TYPES.Rational}, + 34377: {"name": "ImageResources", "type": TYPES.Byte}, + 34665: {"name": "ExifTag", "type": TYPES.Long}, + 34675: {"name": "InterColorProfile", "type": TYPES.Undefined}, + 34853: {"name": "GPSTag", "type": TYPES.Long}, + 34857: {"name": "Interlace", "type": TYPES.Short}, + 34858: {"name": "TimeZoneOffset", "type": TYPES.Long}, + 34859: {"name": "SelfTimerMode", "type": TYPES.Short}, + 37387: {"name": "FlashEnergy", "type": TYPES.Rational}, + 37388: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined}, + 37389: {"name": "Noise", "type": TYPES.Undefined}, + 37390: {"name": "FocalPlaneXResolution", "type": TYPES.Rational}, + 37391: {"name": "FocalPlaneYResolution", "type": TYPES.Rational}, + 37392: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short}, + 37393: {"name": "ImageNumber", "type": TYPES.Long}, + 37394: {"name": "SecurityClassification", "type": TYPES.Ascii}, + 37395: {"name": "ImageHistory", "type": TYPES.Ascii}, + 37397: {"name": "ExposureIndex", "type": TYPES.Rational}, + 37398: {"name": "TIFFEPStandardID", "type": TYPES.Byte}, + 37399: {"name": "SensingMethod", "type": TYPES.Short}, + 40091: {"name": "XPTitle", "type": TYPES.Byte}, + 40092: {"name": "XPComment", "type": TYPES.Byte}, + 40093: {"name": "XPAuthor", "type": TYPES.Byte}, + 40094: {"name": "XPKeywords", "type": TYPES.Byte}, + 40095: {"name": "XPSubject", "type": TYPES.Byte}, + 50341: {"name": "PrintImageMatching", "type": TYPES.Undefined}, + 50706: {"name": "DNGVersion", "type": TYPES.Byte}, + 50707: {"name": "DNGBackwardVersion", "type": TYPES.Byte}, + 50708: {"name": "UniqueCameraModel", "type": TYPES.Ascii}, + 50709: {"name": "LocalizedCameraModel", "type": TYPES.Byte}, + 50710: {"name": "CFAPlaneColor", "type": TYPES.Byte}, + 50711: {"name": "CFALayout", "type": TYPES.Short}, + 50712: {"name": "LinearizationTable", "type": TYPES.Short}, + 50713: {"name": "BlackLevelRepeatDim", "type": TYPES.Short}, + 50714: {"name": "BlackLevel", "type": TYPES.Rational}, + 50715: {"name": "BlackLevelDeltaH", "type": TYPES.SRational}, + 50716: {"name": "BlackLevelDeltaV", "type": TYPES.SRational}, + 50717: {"name": "WhiteLevel", "type": TYPES.Short}, + 50718: {"name": "DefaultScale", "type": TYPES.Rational}, + 50719: {"name": "DefaultCropOrigin", "type": TYPES.Short}, + 50720: {"name": "DefaultCropSize", "type": TYPES.Short}, + 50721: {"name": "ColorMatrix1", "type": TYPES.SRational}, + 50722: {"name": "ColorMatrix2", "type": TYPES.SRational}, + 50723: {"name": "CameraCalibration1", "type": TYPES.SRational}, + 50724: {"name": "CameraCalibration2", "type": TYPES.SRational}, + 50725: {"name": "ReductionMatrix1", "type": TYPES.SRational}, + 50726: {"name": "ReductionMatrix2", "type": TYPES.SRational}, + 50727: {"name": "AnalogBalance", "type": TYPES.Rational}, + 50728: {"name": "AsShotNeutral", "type": TYPES.Short}, + 50729: {"name": "AsShotWhiteXY", "type": TYPES.Rational}, + 50730: {"name": "BaselineExposure", "type": TYPES.SRational}, + 50731: {"name": "BaselineNoise", "type": TYPES.Rational}, + 50732: {"name": "BaselineSharpness", "type": TYPES.Rational}, + 50733: {"name": "BayerGreenSplit", "type": TYPES.Long}, + 50734: {"name": "LinearResponseLimit", "type": TYPES.Rational}, + 50735: {"name": "CameraSerialNumber", "type": TYPES.Ascii}, + 50736: {"name": "LensInfo", "type": TYPES.Rational}, + 50737: {"name": "ChromaBlurRadius", "type": TYPES.Rational}, + 50738: {"name": "AntiAliasStrength", "type": TYPES.Rational}, + 50739: {"name": "ShadowScale", "type": TYPES.SRational}, + 50740: {"name": "DNGPrivateData", "type": TYPES.Byte}, + 50741: {"name": "MakerNoteSafety", "type": TYPES.Short}, + 50778: {"name": "CalibrationIlluminant1", "type": TYPES.Short}, + 50779: {"name": "CalibrationIlluminant2", "type": TYPES.Short}, + 50780: {"name": "BestQualityScale", "type": TYPES.Rational}, + 50781: {"name": "RawDataUniqueID", "type": TYPES.Byte}, + 50827: {"name": "OriginalRawFileName", "type": TYPES.Byte}, + 50828: {"name": "OriginalRawFileData", "type": TYPES.Undefined}, + 50829: {"name": "ActiveArea", "type": TYPES.Short}, + 50830: {"name": "MaskedAreas", "type": TYPES.Short}, + 50831: {"name": "AsShotICCProfile", "type": TYPES.Undefined}, + 50832: {"name": "AsShotPreProfileMatrix", "type": TYPES.SRational}, + 50833: {"name": "CurrentICCProfile", "type": TYPES.Undefined}, + 50834: {"name": "CurrentPreProfileMatrix", "type": TYPES.SRational}, + 50879: {"name": "ColorimetricReference", "type": TYPES.Short}, + 50931: {"name": "CameraCalibrationSignature", "type": TYPES.Byte}, + 50932: {"name": "ProfileCalibrationSignature", "type": TYPES.Byte}, + 50934: {"name": "AsShotProfileName", "type": TYPES.Byte}, + 50935: {"name": "NoiseReductionApplied", "type": TYPES.Rational}, + 50936: {"name": "ProfileName", "type": TYPES.Byte}, + 50937: {"name": "ProfileHueSatMapDims", "type": TYPES.Long}, + 50938: {"name": "ProfileHueSatMapData1", "type": TYPES.Float}, + 50939: {"name": "ProfileHueSatMapData2", "type": TYPES.Float}, + 50940: {"name": "ProfileToneCurve", "type": TYPES.Float}, + 50941: {"name": "ProfileEmbedPolicy", "type": TYPES.Long}, + 50942: {"name": "ProfileCopyright", "type": TYPES.Byte}, + 50964: {"name": "ForwardMatrix1", "type": TYPES.SRational}, + 50965: {"name": "ForwardMatrix2", "type": TYPES.SRational}, + 50966: {"name": "PreviewApplicationName", "type": TYPES.Byte}, + 50967: {"name": "PreviewApplicationVersion", "type": TYPES.Byte}, + 50968: {"name": "PreviewSettingsName", "type": TYPES.Byte}, + 50969: {"name": "PreviewSettingsDigest", "type": TYPES.Byte}, + 50970: {"name": "PreviewColorSpace", "type": TYPES.Long}, + 50971: {"name": "PreviewDateTime", "type": TYPES.Ascii}, + 50972: {"name": "RawImageDigest", "type": TYPES.Undefined}, + 50973: {"name": "OriginalRawFileDigest", "type": TYPES.Undefined}, + 50974: {"name": "SubTileBlockSize", "type": TYPES.Long}, + 50975: {"name": "RowInterleaveFactor", "type": TYPES.Long}, + 50981: {"name": "ProfileLookTableDims", "type": TYPES.Long}, + 50982: {"name": "ProfileLookTableData", "type": TYPES.Float}, + 51008: {"name": "OpcodeList1", "type": TYPES.Undefined}, + 51009: {"name": "OpcodeList2", "type": TYPES.Undefined}, + 51022: {"name": "OpcodeList3", "type": TYPES.Undefined}, + 60606: {"name": "ZZZTestSlong1", "type": TYPES.SLong}, + 60607: {"name": "ZZZTestSlong2", "type": TYPES.SLong}, + 60608: {"name": "ZZZTestSByte", "type": TYPES.SByte}, + 60609: {"name": "ZZZTestSShort", "type": TYPES.SShort}, + 60610: {"name": "ZZZTestDFloat", "type": TYPES.DFloat}, + }, + "Exif": { + 33434: {"name": "ExposureTime", "type": TYPES.Rational}, + 33437: {"name": "FNumber", "type": TYPES.Rational}, + 34850: {"name": "ExposureProgram", "type": TYPES.Short}, + 34852: {"name": "SpectralSensitivity", "type": TYPES.Ascii}, + 34855: {"name": "ISOSpeedRatings", "type": TYPES.Short}, + 34856: {"name": "OECF", "type": TYPES.Undefined}, + 34864: {"name": "SensitivityType", "type": TYPES.Short}, + 34865: {"name": "StandardOutputSensitivity", "type": TYPES.Long}, + 34866: {"name": "RecommendedExposureIndex", "type": TYPES.Long}, + 34867: {"name": "ISOSpeed", "type": TYPES.Long}, + 34868: {"name": "ISOSpeedLatitudeyyy", "type": TYPES.Long}, + 34869: {"name": "ISOSpeedLatitudezzz", "type": TYPES.Long}, + 36864: {"name": "ExifVersion", "type": TYPES.Undefined}, + 36867: {"name": "DateTimeOriginal", "type": TYPES.Ascii}, + 36868: {"name": "DateTimeDigitized", "type": TYPES.Ascii}, + 36880: {"name": "OffsetTime", "type": TYPES.Ascii}, + 36881: {"name": "OffsetTimeOriginal", "type": TYPES.Ascii}, + 36882: {"name": "OffsetTimeDigitized", "type": TYPES.Ascii}, + 37121: {"name": "ComponentsConfiguration", "type": TYPES.Undefined}, + 37122: {"name": "CompressedBitsPerPixel", "type": TYPES.Rational}, + 37377: {"name": "ShutterSpeedValue", "type": TYPES.SRational}, + 37378: {"name": "ApertureValue", "type": TYPES.Rational}, + 37379: {"name": "BrightnessValue", "type": TYPES.SRational}, + 37380: {"name": "ExposureBiasValue", "type": TYPES.SRational}, + 37381: {"name": "MaxApertureValue", "type": TYPES.Rational}, + 37382: {"name": "SubjectDistance", "type": TYPES.Rational}, + 37383: {"name": "MeteringMode", "type": TYPES.Short}, + 37384: {"name": "LightSource", "type": TYPES.Short}, + 37385: {"name": "Flash", "type": TYPES.Short}, + 37386: {"name": "FocalLength", "type": TYPES.Rational}, + 37396: {"name": "SubjectArea", "type": TYPES.Short}, + 37500: {"name": "MakerNote", "type": TYPES.Undefined}, + 37510: {"name": "UserComment", "type": TYPES.Undefined}, + 37520: {"name": "SubSecTime", "type": TYPES.Ascii}, + 37521: {"name": "SubSecTimeOriginal", "type": TYPES.Ascii}, + 37522: {"name": "SubSecTimeDigitized", "type": TYPES.Ascii}, + 37888: {"name": "Temperature", "type": TYPES.SRational}, + 37889: {"name": "Humidity", "type": TYPES.Rational}, + 37890: {"name": "Pressure", "type": TYPES.Rational}, + 37891: {"name": "WaterDepth", "type": TYPES.SRational}, + 37892: {"name": "Acceleration", "type": TYPES.Rational}, + 37893: {"name": "CameraElevationAngle", "type": TYPES.SRational}, + 40960: {"name": "FlashpixVersion", "type": TYPES.Undefined}, + 40961: {"name": "ColorSpace", "type": TYPES.Short}, + 40962: {"name": "PixelXDimension", "type": TYPES.Long}, + 40963: {"name": "PixelYDimension", "type": TYPES.Long}, + 40964: {"name": "RelatedSoundFile", "type": TYPES.Ascii}, + 40965: {"name": "InteroperabilityTag", "type": TYPES.Long}, + 41483: {"name": "FlashEnergy", "type": TYPES.Rational}, + 41484: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined}, + 41486: {"name": "FocalPlaneXResolution", "type": TYPES.Rational}, + 41487: {"name": "FocalPlaneYResolution", "type": TYPES.Rational}, + 41488: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short}, + 41492: {"name": "SubjectLocation", "type": TYPES.Short}, + 41493: {"name": "ExposureIndex", "type": TYPES.Rational}, + 41495: {"name": "SensingMethod", "type": TYPES.Short}, + 41728: {"name": "FileSource", "type": TYPES.Undefined}, + 41729: {"name": "SceneType", "type": TYPES.Undefined}, + 41730: {"name": "CFAPattern", "type": TYPES.Undefined}, + 41985: {"name": "CustomRendered", "type": TYPES.Short}, + 41986: {"name": "ExposureMode", "type": TYPES.Short}, + 41987: {"name": "WhiteBalance", "type": TYPES.Short}, + 41988: {"name": "DigitalZoomRatio", "type": TYPES.Rational}, + 41989: {"name": "FocalLengthIn35mmFilm", "type": TYPES.Short}, + 41990: {"name": "SceneCaptureType", "type": TYPES.Short}, + 41991: {"name": "GainControl", "type": TYPES.Short}, + 41992: {"name": "Contrast", "type": TYPES.Short}, + 41993: {"name": "Saturation", "type": TYPES.Short}, + 41994: {"name": "Sharpness", "type": TYPES.Short}, + 41995: {"name": "DeviceSettingDescription", "type": TYPES.Undefined}, + 41996: {"name": "SubjectDistanceRange", "type": TYPES.Short}, + 42016: {"name": "ImageUniqueID", "type": TYPES.Ascii}, + 42032: {"name": "CameraOwnerName", "type": TYPES.Ascii}, + 42033: {"name": "BodySerialNumber", "type": TYPES.Ascii}, + 42034: {"name": "LensSpecification", "type": TYPES.Rational}, + 42035: {"name": "LensMake", "type": TYPES.Ascii}, + 42036: {"name": "LensModel", "type": TYPES.Ascii}, + 42037: {"name": "LensSerialNumber", "type": TYPES.Ascii}, + 42240: {"name": "Gamma", "type": TYPES.Rational}, + }, + "GPS": { + 0: {"name": "GPSVersionID", "type": TYPES.Byte}, + 1: {"name": "GPSLatitudeRef", "type": TYPES.Ascii}, + 2: {"name": "GPSLatitude", "type": TYPES.Rational}, + 3: {"name": "GPSLongitudeRef", "type": TYPES.Ascii}, + 4: {"name": "GPSLongitude", "type": TYPES.Rational}, + 5: {"name": "GPSAltitudeRef", "type": TYPES.Byte}, + 6: {"name": "GPSAltitude", "type": TYPES.Rational}, + 7: {"name": "GPSTimeStamp", "type": TYPES.Rational}, + 8: {"name": "GPSSatellites", "type": TYPES.Ascii}, + 9: {"name": "GPSStatus", "type": TYPES.Ascii}, + 10: {"name": "GPSMeasureMode", "type": TYPES.Ascii}, + 11: {"name": "GPSDOP", "type": TYPES.Rational}, + 12: {"name": "GPSSpeedRef", "type": TYPES.Ascii}, + 13: {"name": "GPSSpeed", "type": TYPES.Rational}, + 14: {"name": "GPSTrackRef", "type": TYPES.Ascii}, + 15: {"name": "GPSTrack", "type": TYPES.Rational}, + 16: {"name": "GPSImgDirectionRef", "type": TYPES.Ascii}, + 17: {"name": "GPSImgDirection", "type": TYPES.Rational}, + 18: {"name": "GPSMapDatum", "type": TYPES.Ascii}, + 19: {"name": "GPSDestLatitudeRef", "type": TYPES.Ascii}, + 20: {"name": "GPSDestLatitude", "type": TYPES.Rational}, + 21: {"name": "GPSDestLongitudeRef", "type": TYPES.Ascii}, + 22: {"name": "GPSDestLongitude", "type": TYPES.Rational}, + 23: {"name": "GPSDestBearingRef", "type": TYPES.Ascii}, + 24: {"name": "GPSDestBearing", "type": TYPES.Rational}, + 25: {"name": "GPSDestDistanceRef", "type": TYPES.Ascii}, + 26: {"name": "GPSDestDistance", "type": TYPES.Rational}, + 27: {"name": "GPSProcessingMethod", "type": TYPES.Undefined}, + 28: {"name": "GPSAreaInformation", "type": TYPES.Undefined}, + 29: {"name": "GPSDateStamp", "type": TYPES.Ascii}, + 30: {"name": "GPSDifferential", "type": TYPES.Short}, + 31: {"name": "GPSHPositioningError", "type": TYPES.Rational}, + }, + "Interop": {1: {"name": "InteroperabilityIndex", "type": TYPES.Ascii}}, } TAGS["0th"] = TAGS["Image"] TAGS["1st"] = TAGS["Image"] + class ImageIFD: """Exif tag number reference - 0th IFD""" + ProcessingSoftware = 11 NewSubfileType = 254 SubfileType = 255 @@ -516,6 +524,7 @@ class ImageIFD: class ExifIFD: """Exif tag number reference - Exif IFD""" + ExposureTime = 33434 FNumber = 33437 ExposureProgram = 34850 @@ -599,6 +608,7 @@ class ExifIFD: class GPSIFD: """Exif tag number reference - GPS IFD""" + GPSVersionID = 0 GPSLatitudeRef = 1 GPSLatitude = 2 @@ -635,4 +645,5 @@ class GPSIFD: class InteropIFD: """Exif tag number reference - Interoperability IFD""" + InteroperabilityIndex = 1 diff --git a/openflexure_microscope/camera/piexif/_insert.py b/openflexure_microscope/camera/piexif/_insert.py index 2f328653..4330eb66 100644 --- a/openflexure_microscope/camera/piexif/_insert.py +++ b/openflexure_microscope/camera/piexif/_insert.py @@ -6,6 +6,7 @@ from ._common import * from ._exceptions import InvalidImageDataError from . import _webp + def insert(exif, image, new_file=None): """ py:function:: piexif.insert(exif_bytes, filename) @@ -20,7 +21,7 @@ def insert(exif, image, new_file=None): output_file = False # Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2 - maybe_image = sys.version_info >= (3,0,0) or isinstance(image, str) + maybe_image = sys.version_info >= (3, 0, 0) or isinstance(image, str) if maybe_image and image[0:2] == b"\xff\xd8": image_data = image @@ -29,7 +30,7 @@ def insert(exif, image, new_file=None): image_data = image file_type = "webp" else: - with open(image, 'rb') as f: + with open(image, "rb") as f: image_data = f.read() if image_data[0:2] == b"\xff\xd8": file_type = "jpeg" @@ -57,4 +58,4 @@ def insert(exif, image, new_file=None): with open(image, "wb+") as f: f.write(new_data) else: - raise ValueError("Give a 3rd argument to 'insert' to output file") \ No newline at end of file + raise ValueError("Give a 3rd argument to 'insert' to output file") diff --git a/openflexure_microscope/camera/piexif/_load.py b/openflexure_microscope/camera/piexif/_load.py index f7d6e31a..c4143beb 100644 --- a/openflexure_microscope/camera/piexif/_load.py +++ b/openflexure_microscope/camera/piexif/_load.py @@ -19,12 +19,14 @@ def load(input_data, key_is_name=False): :return: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes}) :rtype: dict """ - exif_dict = {"0th":{}, - "Exif":{}, - "GPS":{}, - "Interop":{}, - "1st":{}, - "thumbnail":None} + exif_dict = { + "0th": {}, + "Exif": {}, + "GPS": {}, + "Interop": {}, + "1st": {}, + "thumbnail": None, + } exifReader = _ExifReader(input_data) if exifReader.tiftag is None: return exif_dict @@ -34,8 +36,7 @@ def load(input_data, key_is_name=False): else: exifReader.endian_mark = ">" - pointer = struct.unpack(exifReader.endian_mark + "L", - exifReader.tiftag[4:8])[0] + pointer = struct.unpack(exifReader.endian_mark + "L", exifReader.tiftag[4:8])[0] exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th") first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer") if ImageIFD.ExifTag in exif_dict["0th"]: @@ -48,14 +49,19 @@ def load(input_data, key_is_name=False): pointer = exif_dict["Exif"][ExifIFD.InteroperabilityTag] exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop") if first_ifd_pointer != b"\x00\x00\x00\x00": - pointer = struct.unpack(exifReader.endian_mark + "L", - first_ifd_pointer)[0] + pointer = struct.unpack(exifReader.endian_mark + "L", first_ifd_pointer)[0] exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st") - if (ImageIFD.JPEGInterchangeFormat in exif_dict["1st"] and - ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]): - end = (exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] + - exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength]) - thumb = exifReader.tiftag[exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]:end] + if ( + ImageIFD.JPEGInterchangeFormat in exif_dict["1st"] + and ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"] + ): + end = ( + exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] + + exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] + ) + thumb = exifReader.tiftag[ + exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] : end + ] exif_dict["thumbnail"] = thumb if key_is_name: @@ -66,7 +72,7 @@ def load(input_data, key_is_name=False): class _ExifReader(object): def __init__(self, data): # Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2 - maybe_image = sys.version_info >= (3,0,0) or isinstance(data, str) + maybe_image = sys.version_info >= (3, 0, 0) or isinstance(data, str) if maybe_image and data[0:2] == b"\xff\xd8": # JPEG segments = split_into_segments(data) @@ -82,7 +88,7 @@ class _ExifReader(object): elif maybe_image and data[0:4] == b"Exif": # Exif self.tiftag = data[6:] else: - with open(data, 'rb') as f: + with open(data, "rb") as f: magic_number = f.read(2) if magic_number == b"\xff\xd8": # JPEG app1 = read_exif_from_file(data) @@ -91,13 +97,13 @@ class _ExifReader(object): else: self.tiftag = None elif magic_number in (b"\x49\x49", b"\x4d\x4d"): # TIFF - with open(data, 'rb') as f: + with open(data, "rb") as f: self.tiftag = f.read() else: - with open(data, 'rb') as f: + with open(data, "rb") as f: header = f.read(12) - if header[0:4] == b"RIFF"and header[8:12] == b"WEBP": - with open(data, 'rb') as f: + if header[0:4] == b"RIFF" and header[8:12] == b"WEBP": + with open(data, "rb") as f: file_data = f.read() self.tiftag = _webp.get_exif(file_data) else: @@ -105,8 +111,9 @@ class _ExifReader(object): def get_ifd_dict(self, pointer, ifd_name, read_unknown=False): ifd_dict = {} - tag_count = struct.unpack(self.endian_mark + "H", - self.tiftag[pointer: pointer+2])[0] + tag_count = struct.unpack( + self.endian_mark + "H", self.tiftag[pointer : pointer + 2] + )[0] offset = pointer + 2 if ifd_name in ["0th", "1st"]: t = "Image" @@ -115,26 +122,28 @@ class _ExifReader(object): p_and_value = [] for x in range(tag_count): pointer = offset + 12 * x - tag = struct.unpack(self.endian_mark + "H", - self.tiftag[pointer: pointer+2])[0] - value_type = struct.unpack(self.endian_mark + "H", - self.tiftag[pointer + 2: pointer + 4])[0] - value_num = struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + 4: pointer + 8] - )[0] - value = self.tiftag[pointer+8: pointer+12] + tag = struct.unpack( + self.endian_mark + "H", self.tiftag[pointer : pointer + 2] + )[0] + value_type = struct.unpack( + self.endian_mark + "H", self.tiftag[pointer + 2 : pointer + 4] + )[0] + value_num = struct.unpack( + self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8] + )[0] + value = self.tiftag[pointer + 8 : pointer + 12] p_and_value.append((pointer, value_type, value_num, value)) v_set = (value_type, value_num, value, tag) if tag in TAGS[t]: ifd_dict[tag] = self.convert_value(v_set) elif read_unknown: ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag) - #else: + # else: # pass if ifd_name == "0th": pointer = offset + 12 * tag_count - ifd_dict["first_ifd_pointer"] = self.tiftag[pointer:pointer + 4] + ifd_dict["first_ifd_pointer"] = self.tiftag[pointer : pointer + 4] return ifd_dict def convert_value(self, val): @@ -143,114 +152,148 @@ class _ExifReader(object): length = val[1] value = val[2] - if t == TYPES.Byte: # BYTE + if t == TYPES.Byte: # BYTE if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack("B" * length, - self.tiftag[pointer: pointer + length]) + data = struct.unpack( + "B" * length, self.tiftag[pointer : pointer + length] + ) else: data = struct.unpack("B" * length, value[0:length]) - elif t == TYPES.Ascii: # ASCII + elif t == TYPES.Ascii: # ASCII if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = self.tiftag[pointer: pointer+length - 1] + data = self.tiftag[pointer : pointer + length - 1] else: - data = value[0: length - 1] - elif t == TYPES.Short: # SHORT + data = value[0 : length - 1] + elif t == TYPES.Short: # SHORT if length > 2: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "H" * length, - self.tiftag[pointer: pointer+length*2]) + data = struct.unpack( + self.endian_mark + "H" * length, + self.tiftag[pointer : pointer + length * 2], + ) else: - data = struct.unpack(self.endian_mark + "H" * length, - value[0:length * 2]) - elif t == TYPES.Long: # LONG + data = struct.unpack( + self.endian_mark + "H" * length, value[0 : length * 2] + ) + elif t == TYPES.Long: # LONG if length > 1: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "L" * length, - self.tiftag[pointer: pointer+length*4]) + data = struct.unpack( + self.endian_mark + "L" * length, + self.tiftag[pointer : pointer + length * 4], + ) else: - data = struct.unpack(self.endian_mark + "L" * length, - value) - elif t == TYPES.Rational: # RATIONAL + data = struct.unpack(self.endian_mark + "L" * length, value) + elif t == TYPES.Rational: # RATIONAL pointer = struct.unpack(self.endian_mark + "L", value)[0] if length > 1: data = tuple( - (struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + x * 8: - pointer + 4 + x * 8])[0], - struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + 4 + x * 8: - pointer + 8 + x * 8])[0]) + ( + struct.unpack( + self.endian_mark + "L", + self.tiftag[pointer + x * 8 : pointer + 4 + x * 8], + )[0], + struct.unpack( + self.endian_mark + "L", + self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8], + )[0], + ) for x in range(length) ) else: - data = (struct.unpack(self.endian_mark + "L", - self.tiftag[pointer: pointer + 4])[0], - struct.unpack(self.endian_mark + "L", - self.tiftag[pointer + 4: pointer + 8] - )[0]) - elif t == TYPES.SByte: # SIGNED BYTES + data = ( + struct.unpack( + self.endian_mark + "L", self.tiftag[pointer : pointer + 4] + )[0], + struct.unpack( + self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8] + )[0], + ) + elif t == TYPES.SByte: # SIGNED BYTES if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack("b" * length, - self.tiftag[pointer: pointer + length]) + data = struct.unpack( + "b" * length, self.tiftag[pointer : pointer + length] + ) else: data = struct.unpack("b" * length, value[0:length]) - elif t == TYPES.Undefined: # UNDEFINED BYTES + elif t == TYPES.Undefined: # UNDEFINED BYTES if length > 4: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = self.tiftag[pointer: pointer+length] + data = self.tiftag[pointer : pointer + length] else: data = value[0:length] - elif t == TYPES.SShort: # SIGNED SHORT + elif t == TYPES.SShort: # SIGNED SHORT if length > 2: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "h" * length, - self.tiftag[pointer: pointer+length*2]) + data = struct.unpack( + self.endian_mark + "h" * length, + self.tiftag[pointer : pointer + length * 2], + ) else: - data = struct.unpack(self.endian_mark + "h" * length, - value[0:length * 2]) - elif t == TYPES.SLong: # SLONG + data = struct.unpack( + self.endian_mark + "h" * length, value[0 : length * 2] + ) + elif t == TYPES.SLong: # SLONG if length > 1: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "l" * length, - self.tiftag[pointer: pointer+length*4]) + data = struct.unpack( + self.endian_mark + "l" * length, + self.tiftag[pointer : pointer + length * 4], + ) else: - data = struct.unpack(self.endian_mark + "l" * length, - value) - elif t == TYPES.SRational: # SRATIONAL + data = struct.unpack(self.endian_mark + "l" * length, value) + elif t == TYPES.SRational: # SRATIONAL pointer = struct.unpack(self.endian_mark + "L", value)[0] if length > 1: data = tuple( - (struct.unpack(self.endian_mark + "l", - self.tiftag[pointer + x * 8: pointer + 4 + x * 8])[0], - struct.unpack(self.endian_mark + "l", - self.tiftag[pointer + 4 + x * 8: pointer + 8 + x * 8])[0]) - for x in range(length) + ( + struct.unpack( + self.endian_mark + "l", + self.tiftag[pointer + x * 8 : pointer + 4 + x * 8], + )[0], + struct.unpack( + self.endian_mark + "l", + self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8], + )[0], + ) + for x in range(length) ) else: - data = (struct.unpack(self.endian_mark + "l", - self.tiftag[pointer: pointer + 4])[0], - struct.unpack(self.endian_mark + "l", - self.tiftag[pointer + 4: pointer + 8] - )[0]) - elif t == TYPES.Float: # FLOAT + data = ( + struct.unpack( + self.endian_mark + "l", self.tiftag[pointer : pointer + 4] + )[0], + struct.unpack( + self.endian_mark + "l", self.tiftag[pointer + 4 : pointer + 8] + )[0], + ) + elif t == TYPES.Float: # FLOAT if length > 1: pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "f" * length, - self.tiftag[pointer: pointer+length*4]) + data = struct.unpack( + self.endian_mark + "f" * length, + self.tiftag[pointer : pointer + length * 4], + ) else: - data = struct.unpack(self.endian_mark + "f" * length, - value) - elif t == TYPES.DFloat: # DOUBLE + data = struct.unpack(self.endian_mark + "f" * length, value) + elif t == TYPES.DFloat: # DOUBLE pointer = struct.unpack(self.endian_mark + "L", value)[0] - data = struct.unpack(self.endian_mark + "d" * length, - self.tiftag[pointer: pointer+length*8]) + data = struct.unpack( + self.endian_mark + "d" * length, + self.tiftag[pointer : pointer + length * 8], + ) else: - raise ValueError("Exif might be wrong. Got incorrect value " + - "type to decode.\n" + - "tag: " + str(val[3]) + "\ntype: " + str(t)) + raise ValueError( + "Exif might be wrong. Got incorrect value " + + "type to decode.\n" + + "tag: " + + str(val[3]) + + "\ntype: " + + str(t) + ) if isinstance(data, tuple) and (len(data) == 1): return data[0] @@ -260,11 +303,20 @@ class _ExifReader(object): def _get_key_name_dict(exif_dict): new_dict = { - "0th":{TAGS["Image"][n]["name"]:value for n, value in exif_dict["0th"].items()}, - "Exif":{TAGS["Exif"][n]["name"]:value for n, value in exif_dict["Exif"].items()}, - "1st":{TAGS["Image"][n]["name"]:value for n, value in exif_dict["1st"].items()}, - "GPS":{TAGS["GPS"][n]["name"]:value for n, value in exif_dict["GPS"].items()}, - "Interop":{TAGS["Interop"][n]["name"]:value for n, value in exif_dict["Interop"].items()}, - "thumbnail":exif_dict["thumbnail"], + "0th": { + TAGS["Image"][n]["name"]: value for n, value in exif_dict["0th"].items() + }, + "Exif": { + TAGS["Exif"][n]["name"]: value for n, value in exif_dict["Exif"].items() + }, + "1st": { + TAGS["Image"][n]["name"]: value for n, value in exif_dict["1st"].items() + }, + "GPS": {TAGS["GPS"][n]["name"]: value for n, value in exif_dict["GPS"].items()}, + "Interop": { + TAGS["Interop"][n]["name"]: value + for n, value in exif_dict["Interop"].items() + }, + "thumbnail": exif_dict["thumbnail"], } - return new_dict \ No newline at end of file + return new_dict diff --git a/openflexure_microscope/camera/piexif/_remove.py b/openflexure_microscope/camera/piexif/_remove.py index e89cda42..10940b96 100644 --- a/openflexure_microscope/camera/piexif/_remove.py +++ b/openflexure_microscope/camera/piexif/_remove.py @@ -3,6 +3,7 @@ import io from ._common import * from . import _webp + def remove(src, new_file=None): """ py:function:: piexif.remove(filename) @@ -19,7 +20,7 @@ def remove(src, new_file=None): src_data = src file_type = "webp" else: - with open(src, 'rb') as f: + with open(src, "rb") as f: src_data = f.read() output_is_file = True if src_data[0:2] == b"\xff\xd8": @@ -39,8 +40,8 @@ def remove(src, new_file=None): new_data = _webp.remove(src_data) except ValueError: new_data = src_data - except e: - print(e.args) + except Exception as e: + print(e) raise ValueError("Error occurred.") if isinstance(new_file, io.BytesIO): @@ -53,4 +54,4 @@ def remove(src, new_file=None): with open(src, "wb+") as f: f.write(new_data) else: - raise ValueError("Give a second argument to 'remove' to output file") \ No newline at end of file + raise ValueError("Give a second argument to 'remove' to output file") diff --git a/openflexure_microscope/camera/piexif/_transplant.py b/openflexure_microscope/camera/piexif/_transplant.py index 0abf7f11..8f2a0a4f 100644 --- a/openflexure_microscope/camera/piexif/_transplant.py +++ b/openflexure_microscope/camera/piexif/_transplant.py @@ -15,7 +15,7 @@ def transplant(exif_src, image, new_file=None): if exif_src[0:2] == b"\xff\xd8": src_data = exif_src else: - with open(exif_src, 'rb') as f: + with open(exif_src, "rb") as f: src_data = f.read() segments = split_into_segments(src_data) exif = get_exif_seg(segments) @@ -26,7 +26,7 @@ def transplant(exif_src, image, new_file=None): if image[0:2] == b"\xff\xd8": image_data = image else: - with open(image, 'rb') as f: + with open(image, "rb") as f: image_data = f.read() output_file = True segments = split_into_segments(image_data) @@ -42,4 +42,4 @@ def transplant(exif_src, image, new_file=None): with open(image, "wb+") as f: f.write(new_data) else: - raise ValueError("Give a 3rd argument to 'transplant' to output file") \ No newline at end of file + raise ValueError("Give a 3rd argument to 'transplant' to output file") diff --git a/openflexure_microscope/camera/piexif/_webp.py b/openflexure_microscope/camera/piexif/_webp.py index f9cc42ea..10649edf 100644 --- a/openflexure_microscope/camera/piexif/_webp.py +++ b/openflexure_microscope/camera/piexif/_webp.py @@ -1,4 +1,3 @@ - import struct @@ -18,26 +17,33 @@ def split(data): chunks = [] while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size: - fourcc = data[pointer:pointer + CHUNK_FOURCC_LENGTH] + fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH] pointer += CHUNK_FOURCC_LENGTH - chunk_length_bytes = data[pointer:pointer + LENGTH_BYTES_LENGTH] + chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH] chunk_length = struct.unpack("> 5-1 & ord(b"\x01") + flag = ord(chunk_data[4:5]) >> 5 - 1 & ord(b"\x01") contains = 1 * flag return contains + def _get_size_from_vp8L(chunk): b1 = chunk["data"][1:2] b2 = chunk["data"][2:3] @@ -79,11 +90,14 @@ def _get_size_from_vp8L(chunk): width_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1) width = width_minus_one + 1 - height_minus_one = (ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6 + height_minus_one = ( + (ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6 + ) height = height_minus_one + 1 return (width, height) + def _get_size_from_anmf(chunk): width_minus_one_bytes = chunk["data"][6:9] + b"\x00" width_minus_one = struct.unpack(" None: + """Create a new StreamObject, to manage capture data.""" + # Stream for buffering capture data + self.stream = io.BytesIO() + + # Store a nice ID + self.id = uuid.uuid4() #: str: Unique capture ID + logging.debug("Created StreamObject {}".format(self.id)) + self.datetime = datetime.datetime.now() + + # Create file name. Default to UUID + self.file = filepath + self.split_file_path(self.file) + + if not os.path.exists(self.filefolder): + os.makedirs(self.filefolder) + + # Dictionary for adding top-level metadata (cannmot be accessed through web API) + self._metadata = {} + + # Dictionary for storing custom annotations + self.annotations = {} + # List for storing tags + self.tags = [] + + # Thumbnail (populated only for PIL captures) + self.thumb_bytes = None + + def write(self, s): + self.stream.write(s) + + def flush(self): + logging.info(f"Writing to disk {self.file}") + with open(self.file, "wb") as outfile: + outfile.write(self.stream.getbuffer()) + self.stream.close() + logging.info(f"Finished writing to disk {self.file}") + + def open(self, mode): + return open(self.file, mode) + + def split_file_path(self, filepath): + """ + Take a full file path, and split it into separated class properties. + + Args: + filepath (str): String of the full file path, including file format extension + """ + # Split the full file path into a folder and a name + self.filefolder, self.name = os.path.split(filepath) + # Split the name out from it's file extension + self.basename = os.path.splitext(self.name)[0] + self.format = self.name.split(".")[-1] + + @property + def exists(self) -> bool: + """Check if capture data file exists on disk.""" + if os.path.isfile(self.file): + return True + else: + return False + + # HANDLE TAGS + def put_tags(self, tags: list): + """ + Add a new tag to the ``tags`` list attribute. + + Args: + tags (list): List of tags to be added + """ + for tag in tags: + if tag not in self.tags: + self.tags.append(tag) + + self.save_metadata() + + def delete_tag(self, tag: str): + """ + Remove a tag from the ``tags`` list attribute, if it exists. + + Args: + tag (str): Tag to be removed + """ + if tag in self.tags: + self.tags = [new_tag for new_tag in self.tags if new_tag != tag] + + self.save_metadata() + + # HANDLE METADATA + + def put_annotations(self, data: dict) -> None: + """ + Merge annotations from a passed dictionary into the capture metadata, and saves. + + Args: + data (dict): Dictionary of metadata to be added + """ + self.annotations.update(data) + self.save_metadata() + + def put_metadata(self, data: dict) -> None: + """ + Merge root metadata from a passed dictionary into the capture metadata, and saves. + + Args: + data (dict): Dictionary of metadata to be added + """ + self._metadata.update(data) + self.save_metadata() + + def put_and_save(self, tags: list = None, annotations: dict = None, metadata: dict = None): + """ + Batch-write tags, metadata, and annotations in a single disk operation + """ + if not tags: + tags = [] + if not annotations: + annotations = {} + if not metadata: + metadata = {} + + # Tags + for tag in tags: + if tag not in self.tags: + self.tags.append(tag) + # Annotations + self.annotations.update(annotations) + # Metadata + self._metadata.update(metadata) + + self.save_metadata() + + def save_metadata(self) -> None: + """ + Save metadata to exif, if supported + """ + global EXIF_FORMATS + + if self.format.upper() in EXIF_FORMATS and self.exists: + logging.debug("Writing exif data to capture file") + # Extract current Exif data + exif_dict = piexif.load(self.file) + # Serialize metadata + metadata_string = json.dumps(self.metadata, cls=JSONEncoder) + logging.debug(f"Saving metadata string to file: {metadata_string}") + # Insert metadata into exif_dict + exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode() + # Convert new exif dict to exif bytes + exif_bytes = piexif.dump(exif_dict) + # Insert exif into file + piexif.insert(exif_bytes, self.file) + logging.info(f"Finished saving metadata to {self.file}") + + @property + def metadata(self) -> dict: + """ + Create basic metadata dictionary from basic capture data, + and any added custom metadata and tags. + """ + d = { + "image": { + "id": self.id, + "name": self.name, + "acquisitionDate": self.datetime.isoformat(), + "format": self.format, + "tags": self.tags, + "annotations": self.annotations, + }, + **self._metadata, + } + + # Add custom metadata to dictionary + return d + + @property + def state(self) -> dict: + """ + Return a dictionary of objects full state, including metadata. + """ + + # Create basic state dictionary + d = {"path": self.file, "name": self.name, "metadata": self.metadata} + + # Combined availability of data + if self.exists: + d["available"] = True + else: + d["available"] = False + + return d + + @property + def data(self) -> io.BytesIO: + """ + Return a byte string of the capture data. + """ + + if self.exists: # If data file exists + logging.info("Opening from file {}".format(self.file)) + with open(self.file, "rb") as f: + d = io.BytesIO(f.read()) # Load bytes from file + d.seek(0) # Rewind loaded bytestream + # Create a copy of the bytestream bytes + data = io.BytesIO(d.getbuffer()) + else: + data = None + + return data # Read and return bytes data + + @property + def binary(self) -> bytes: + """Return a byte string of the capture data.""" + return self.data.getvalue() + + @property + def thumbnail(self) -> io.BytesIO: + """ + Returns a thumbnail of the capture data, for supported image formats. + """ + global THUMBNAIL_SIZE + # If no thumbnail exists, try and make one + if not self.thumb_bytes: + logging.info("Building thumbnail") + self.thumb_bytes = io.BytesIO() + if self.format.upper() in PIL_FORMATS: + im = Image.open(self.data) + im.thumbnail(THUMBNAIL_SIZE) + im.save(self.thumb_bytes, self.format) + self.thumb_bytes.seek(0) + else: + self.thumb_bytes.seek(0) + + # Copy the buffer, to avoid closing the file + data = io.BytesIO(self.thumb_bytes.getbuffer()) + return data + + def save(self) -> None: + """Write stream to file, and save/update metadata file""" + # If a stream OR file exists, save the metadata file + if self.exists: + self.save_metadata() + + def delete(self) -> bool: + """If the StreamObject has been saved, delete the file.""" + + if os.path.isfile(self.file): + logging.info("Deleting file {}".format(self.file)) + os.remove(self.file) + + return True + else: + return False + + def close(self): + pass diff --git a/openflexure_microscope/captures/capture_manager.py b/openflexure_microscope/captures/capture_manager.py new file mode 100644 index 00000000..3d88396a --- /dev/null +++ b/openflexure_microscope/captures/capture_manager.py @@ -0,0 +1,210 @@ +import os +import datetime +import shutil +import logging +from collections import OrderedDict + +from labthings import StrictLock + +from openflexure_microscope.utilities import entry_by_uuid +from openflexure_microscope.paths import data_file_path + +from .capture import CaptureObject, build_captures_from_exif + +BASE_CAPTURE_PATH = data_file_path("micrographs") +TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp") + + +def last_entry(object_list: list): + """Return the last entry of a list, if the list contains items.""" + if object_list: # If any images have been captured + return object_list[-1] # Return the latest captured image + else: + return None + + +def generate_basename(): + """Return a default filename based on the capture datetime""" + return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + +def generate_numbered_basename(obj_list: list) -> str: + initial_basename = generate_basename() + basename = initial_basename + # Handle clashing + iterator = 1 + while basename in [obj.basename for obj in obj_list]: + basename = initial_basename + "_{}".format(iterator) + iterator += 1 + + return basename + + +class CaptureManager: + def __init__(self): + self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH} + + self.lock = StrictLock(timeout=1, name="Captures") + + # Capture data + self.images = OrderedDict() + self.videos = OrderedDict() + + # FILE MANAGEMENT + + def __enter__(self): + """Create camera on context enter.""" + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Close camera stream on context exit.""" + self.close() + + def close(self): + logging.info("Closing {}".format(self)) + # Close all StreamObjects + for capture_list in [self.images.values(), self.videos.values()]: + for stream_object in capture_list: + stream_object.close() + # Empty temp directory + self.clear_tmp() + + def clear_tmp(self): + """ + Removes all files in the temporary capture directories + """ + + if os.path.isdir(self.paths["temp"]): + logging.info("Clearing {}...".format(self.paths["temp"])) + shutil.rmtree(self.paths["temp"]) + logging.debug("Cleared {}.".format(self.paths["temp"])) + + def rebuild_captures(self): + self.images = build_captures_from_exif(self.paths["default"]) + + def update_settings(self, config: dict): + """Update settings from a config dictionary""" + with self.lock: + # Apply valid config params to camera object + for key, value in config.items(): # For each provided setting + if hasattr(self, key): # If the instance has a matching property + setattr(self, key, value) # Set to the target value + + def read_settings(self) -> dict: + """Return the current settings as a dictionary""" + return {"paths": self.paths} + + # RETURNING CAPTURES + + @property + def image(self): + """Return the latest captured image.""" + return last_entry(self.images.values()) + + @property + def video(self): + """Return the latest recorded video.""" + return last_entry(self.videos.values()) + + def image_from_id(self, image_id): + """Return an image StreamObject with a matching ID.""" + logging.warning("image_from_id is deprecated. Access captures as a dictionary.") + return entry_by_uuid(image_id, self.images.values()) + + def video_from_id(self, video_id): + """Return a video StreamObject with a matching ID.""" + logging.warning("video_from_id is deprecated. Access captures as a dictionary.") + return entry_by_uuid(video_id, self.videos.values()) + + # CREATING NEW CAPTURES + + def new_image( + self, + temporary: bool = True, + filename: str = None, + folder: str = "", + fmt: str = "jpeg", + ): + + """ + Create a new image capture object. + + Args: + temporary (bool): Should the data be deleted after session ends. + Creating the capture with a content manager sets this to true. + filename (str): Name of the stored file. Defaults to timestamp. + folder (str): Name of the folder in which to store the capture. + fmt (str): Format of the capture. + """ + + # Generate file name + if not filename: + filename = generate_numbered_basename(self.images.values()) + logging.debug(filename) + filename = "{}.{}".format(filename, fmt) + + # Generate folder + base_folder = self.paths["temp"] if temporary else self.paths["default"] + folder = os.path.join(base_folder, folder) + + # Generate file path + filepath = os.path.join(folder, filename) + + # Create capture object + output = CaptureObject(filepath=filepath) + # Insert a temporary tag if temporary + if temporary: + output.put_tags(["temporary"]) + + # Update capture list + capture_key = str(output.id) + logging.debug(f"Adding image {output} with key {capture_key}") + self.images[capture_key] = output + + return output + + def new_video( + self, + temporary: bool = False, + filename: str = None, + folder: str = "", + fmt: str = "h264", + ): + + """ + Create a new video capture object. + + Args: + temporary (bool): Should the data be deleted after session ends. + Creating the capture with a content manager sets this to true. + filename (str): Name of the stored file. Defaults to timestamp. + folder (str): Name of the folder in which to store the capture. + fmt (str): Format of the capture. + """ + # TODO: Remove the redundancy here + + # Generate file name + if not filename: + filename = generate_numbered_basename(self.videos.values()) + logging.debug(filename) + filename = "{}.{}".format(filename, fmt) + + # Generate folder + base_folder = self.paths["temp"] if temporary else self.paths["default"] + folder = os.path.join(base_folder, folder) + + # Generate file path + filepath = os.path.join(folder, filename) + + # Create capture object + output = CaptureObject(filepath=filepath) + # Insert a temporary tag if temporary + if temporary: + output.put_tags(["temporary"]) + + # Update capture list + capture_key = str(output.id) + logging.debug(f"Adding video {output} with key {capture_key}") + self.videos[capture_key] = output + + return output diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 84fff0ff..ce6382db 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -1,157 +1,159 @@ -import yaml +import json +import flask import os import errno import logging import shutil -import copy +from uuid import UUID +import numpy as np from fractions import Fraction -from collections import abc -""" -Attributes: - USER_CONFIG_DIR (str): Default path of the user-config directory, containing runtime-config and - calibration files. Obtained from ``os.path.join(os.path.expanduser("~"), ".openflexure")``. - USER_CONFIG_FILE (str): Default path of the user microscope_settings.yaml runtime-config file. - Obtained from ``os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")`` -""" +from labthings.json import LabThingsJSONEncoder -# HANDLE THE DEFAULT CONFIGURATION FILE - -HERE = os.path.abspath(os.path.dirname(__file__)) -DEFAULT_CONFIG_PATH = os.path.join(HERE, 'microscope_settings.default.yaml') - -USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure") -USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml") - -USER_CONFIG_FILE_OLD = os.path.join(USER_CONFIG_DIR, "microscoperc.yaml") - -# Load the default config -with open(DEFAULT_CONFIG_PATH, 'r') as default_rc: - DEFAULT_CONFIG = default_rc.read() - -# Run a one-time conversion of the old microscoperc.yaml into the new microscope_settings.yaml -# TODO: Should be removed in >1.0.1? -if (not os.path.isfile(USER_CONFIG_FILE)) and (os.path.isfile(USER_CONFIG_FILE_OLD)): - os.rename(USER_CONFIG_FILE_OLD, USER_CONFIG_FILE) +from .paths import ( + SETTINGS_FILE_PATH, + DEFAULT_SETTINGS_FILE_PATH, + CONFIGURATION_FILE_PATH, + DEFAULT_CONFIGURATION_FILE_PATH, +) -# HANDLE CUSTOM YAML PARSING -def construct_yaml_bool(self, node): +class OpenflexureSettingsFile: """ - Override PyYAML constructors handling of bools. - YAML otherwise caused grief for picamera properties where 'off' is a valid string value. - """ - override_bool_values = { - 'yes': True, - 'no': False, - 'true': True, - 'false': False, - 'on': 'on', - 'off': 'off', - } - value = self.construct_scalar(node) - return override_bool_values[value.lower()] - - -yaml.Loader.add_constructor(u'tag:yaml.org,2002:bool', construct_yaml_bool) - - -# HANDLE CONVERTING ARBITRARY CONFIG TO JSON-SAFE JSON - -def convert_type_to_json_safe(v): - """Make an individual attribute JSON-safe""" - if isinstance(v, Fraction): - return float(v) - else: - return v - - -def recursively_apply(data, func): - """ - Recursively apply a function to a dictionary, list, array, or tuple + An object to handle expansion, conversion, and saving of the microscope configuration. Args: - data: Input iterable data - func: Function to apply to all non-iterable values + config_path (str): Path to the config JSON file (None falls back to default location) + expand (bool): Expand paths to valid auxillary config files. """ - # If the object is a dictionary - if isinstance(data, abc.Mapping): - return {key: recursively_apply(val, func) for key, val in data.items()} - # If the object is iterable but NOT a dictionary or a string - elif (isinstance(data, abc.Iterable) and - not isinstance(data, abc.Mapping) and - not isinstance(data, str)): - return [recursively_apply(x, func) for x in data] - # if the object is neither a map nor iterable - else: - return func(data) + + def __init__(self, path: str, defaults: dict = {}): + global DEFAULT_SETTINGS + + # Set arguments + self.path = path + + # Initialise basic config file with defaults if it doesn't exist + initialise_file(self.path, populate=defaults) + + def load(self) -> dict: + """ + Loads settings from a file on-disk. + """ + # Unexpanded config dictionary (used at load/save time) + loaded_config = load_json_file(self.path) + + logging.debug("Reading settings from disk") + return loaded_config + + def save(self, config: dict, backup: bool = True): + """ + Save settings to a file on-disk. + + Args: + config (dict): Dictionary of new settings + backup (bool): Back up previous settings file + """ + + save_settings = config + + if backup: + if os.path.isfile(self.path): + shutil.copyfile(self.path, self.path + ".bk") + + logging.debug("Saving settings dictionary to disk") + save_json_file(self.path, save_settings) + + def merge(self, config: dict) -> dict: + """ + Merge settings dictionary with settings loaded from file on-disk. + + Args: + config (dict): Dictionary of new settings + """ + + logging.debug("Merging settings with file on disk") + settings = self.load() + settings.update(config) + + return settings -def settings_to_json(data: dict, clean_keys=True): +class JSONEncoder(LabThingsJSONEncoder): """ - Make a copy of an input dictionary that's safe for JSON return + A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays + """ + + def default(self, o, markers=None): + if isinstance(o, UUID): + return str(o) + # PiCamera fractions + elif isinstance(o, Fraction): + return float(o) + # Numpy integers + elif isinstance(o, np.integer): + return int(o) + # Numpy arrays + elif isinstance(o, np.ndarray): + return o.tolist() + # UUIDs + elif isinstance(o, UUID): + return str(o) + else: + # call base class implementation which takes care of + # raising exceptions for unsupported types + return LabThingsJSONEncoder.default(self, o) + + +# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES + + +def load_json_file(config_path) -> dict: + """ + Open a .json config file Args: - data: Input dictionary - clean_keys: Modify any keys unsuitable for JSON return - """ - - # Do not overwrite original data dictionary - d = copy.deepcopy(data) - - # If we're cleaning up unsuitable keys - if clean_keys: - # Convert lens_shading_table to a bool - if 'picamera_settings' in d['camera_settings'] and 'lens_shading_table' in d['camera_settings']['picamera_settings']: - logging.debug("Bool-ifying lens_shading_table") - if d['camera_settings']['picamera_settings']['lens_shading_table'] is not None: - d['camera_settings']['picamera_settings']['lens_shading_table'] = True - - return recursively_apply(d, convert_type_to_json_safe) - - -# HANDLE BASIC LOADING AND SAVING OF YAML FILES - -def load_yaml_file(config_path) -> dict: - """ - Open a .yaml config file - - Args: - config_path (str): Path to the config YAML file. If `None`, defaults to `DEFAULT_CONFIG_PATH` + config_path (str): Path to the config JSON file. If `None`, defaults to `DEFAULT_CONFIG_PATH` """ config_path = os.path.expanduser(config_path) logging.info("Loading {}...".format(config_path)) with open(config_path) as config_file: - config_data = yaml.load(config_file) + try: + config_data = json.load(config_file) + except json.decoder.JSONDecodeError as e: + logging.error(e) + config_data = {} # Return loaded config dictionary return config_data -def save_yaml_file(config_path: str, config_dict: dict, safe: bool = False): +def save_json_file(config_path: str, config_dict: dict): """ - Save a .yaml config file + Save a .json config file Args: config_dict (dict): Dictionary of config data to save. - config_path (str): Path to the config YAML file. - safe (bool): Whether to use PyYAML safe_dump instead of dump + config_path (str): Path to the config JSON file. """ config_path = os.path.expanduser(config_path) logging.info("Saving {}...".format(config_path)) logging.debug(config_dict) - with open(config_path, 'w') as outfile: - if not safe: - yaml.dump(config_dict, outfile) - else: - yaml.safe_dump(config_dict, outfile) + with open(config_path, "w") as outfile: + json.dump(config_dict, outfile, cls=JSONEncoder, indent=2, sort_keys=True) def create_file(config_path): + """ + Creates an empty file, and all folder structure currently nonexistant. + + Args: + config_path: Path to the (possibly) new file + """ if not os.path.exists(os.path.dirname(config_path)): try: os.makedirs(os.path.dirname(config_path)) @@ -160,7 +162,7 @@ def create_file(config_path): raise -def initialise_file(config_path, populate: str = ""): +def initialise_file(config_path, populate: str = "{}\n"): """ Check if a file exists, and if not, create it and optionally populate it with content @@ -179,127 +181,25 @@ def initialise_file(config_path, populate: str = ""): create_file(config_path) logging.info("Populating {}...".format(config_path)) - with open(config_path, 'w') as outfile: + with open(config_path, "w") as outfile: outfile.write(populate) -# MAIN CONFIG CLASS -class OpenflexureSettingsFile: - """ - An object to handle expansion, conversion, and saving of the microscope configuration. +# Load the default settings +with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings: + DEFAULT_SETTINGS = default_settings.read() - Args: - config_path (str): Path to the config YAML file (None falls back to default location) - expand (bool): Expand paths to valid auxillary config files. - """ - def __init__(self, config_path: str = None, expand: bool = True): - global DEFAULT_CONFIG, USER_CONFIG_FILE +#: Default user settings object +user_settings = OpenflexureSettingsFile( + path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS +) - self.expandable_keys = { - 'camera_settings': None, - 'stage_settings': None - } #: Dictionary of keys that can be passed as a file path string and expanded automatically - # Set arguments - self.config_path = config_path or USER_CONFIG_FILE - self.expand = expand +# Load the default configuration +with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration: + DEFAULT_CONFIGURATION = default_configuration.read() - # Initialise basic config file with defaults if it doesn't exist - initialise_file(self.config_path, populate=DEFAULT_CONFIG) - - def load(self): - """ - Loads config from a file on-disk, and expands auxillary config files if available. - """ - # Unexpanded config dictionary (used at load/save time) - loaded_config = load_yaml_file(self.config_path) - - # If the loaded config is in contracted format - if self.expand: - # Expand self.raw_config into self._config - loaded_config = self.expand_config(loaded_config) - - logging.debug("Reading settings from disk") - return loaded_config - - def save(self, config: dict, backup: bool = True): - """ - Save config to a file on-disk, and splits into auxillary config files if available. - """ - # If the loaded config was in contracted format - if self.expand: - # Contract self._config into self.raw_config - save_config = self.contract_config(config) - else: - save_config = config - - if backup: - if os.path.isfile(self.config_path): - shutil.copyfile(self.config_path, self.config_path+".bk") - - logging.debug("Saving settings dictionary to disk") - save_yaml_file(self.config_path, save_config) - - def merge(self, config: dict, backup: bool = True): - logging.debug("Merging settings with file on disk") - settings = self.load() - settings.update(config) - - return settings - - def expand_config(self, config_dict): - """ - Search a config dictionary for paths to valid auxillary config files, - and expand into the config dictionary if available. - - Args: - config_dict (dict): Dictionary of config data to expand - """ - return_config = {} - if not config_dict: - config_dict = {} - # For each value in the raw loaded config - for key, value in config_dict.items(): - # If it's a valid expandable parameter - if (key in self.expandable_keys and - type(value) is str): - - logging.debug("Expanding {}".format(value)) - - # Store expansion path - self.expandable_keys[key] = config_dict[key] - # Create the expansion file if it doesn't yet exist - initialise_file(value) - # Load the expansion file into _config - return_config[key] = load_yaml_file(value) or {} - else: - return_config[key] = value - - return return_config - - def contract_config(self, config_dict): - """ - Split a config dictionary into auxillary config files, if available. - - Args: - config_dict (dict): Dictionary of config data to contract/split - """ - return_config = {} - # For each value in the expanded config - for key, value in config_dict.items(): - # If it's a valid expandable parameter - if (key in self.expandable_keys and - self.expandable_keys[key] is not None and - type(value) is dict): - - logging.debug("Saving to {}".format(self.expandable_keys[key])) - # Create the file if it doesn't exist - initialise_file(self.expandable_keys[key]) - # Save the expanded config dictionary to the file - save_yaml_file(self.expandable_keys[key], value) - # Replace the expanded dictionary with a file path - return_config[key] = self.expandable_keys[key] - else: - return_config[key] = value - - return return_config +#: Default user settings object +user_configuration = OpenflexureSettingsFile( + path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION +) diff --git a/openflexure_microscope/devel/__init__.py b/openflexure_microscope/devel/__init__.py index df94377a..93f1aba9 100644 --- a/openflexure_microscope/devel/__init__.py +++ b/openflexure_microscope/devel/__init__.py @@ -5,13 +5,26 @@ Here we include some classes used frequently in plugin development, as well as some Flask imports to simplify API route development """ -# Plugin classes -from openflexure_microscope.plugins import MicroscopePlugin -from openflexure_microscope.api.v1.views import MicroscopeViewPlugin -from openflexure_microscope.api.utilities import JsonPayload +from openflexure_microscope.api.utilities import JsonResponse + +# Task management +from labthings import ( + current_action as current_task, + update_action_progress as update_task_progress, + update_action_data as update_task_data, +) -# Exceptions -from openflexure_microscope.exceptions import TaskDeniedException # Flask things -from flask import abort, escape, jsonify, Response, request +from flask import abort, escape, Response, request + + +__all__ = [ + "current_task", + "update_task_progress", + "update_task_data", + "abort", + "escape", + "Response", + "request", +] diff --git a/openflexure_microscope/exceptions.py b/openflexure_microscope/exceptions.py index b03bc88d..b85196d8 100644 --- a/openflexure_microscope/exceptions.py +++ b/openflexure_microscope/exceptions.py @@ -1,14 +1,10 @@ from threading import ThreadError -class TaskDeniedException(Exception): - pass - - class LockError(ThreadError): ERROR_CODES = { - 'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.", - 'IN_USE_ERROR': "Lock in use by another thread." + "ACQUIRE_ERROR": "Unable to acquire. Lock in use by another thread.", + "IN_USE_ERROR": "Lock in use by another thread.", } def __init__(self, code, lock): diff --git a/openflexure_microscope/install.py b/openflexure_microscope/install.py new file mode 100644 index 00000000..e73037cc --- /dev/null +++ b/openflexure_microscope/install.py @@ -0,0 +1,14 @@ +from pynpm import NPMPackage +import os + +dir_path = os.path.dirname(os.path.realpath(__file__)) +pkg_path = os.path.join(dir_path, "api", "static", "package.json") + +pkg = NPMPackage(pkg_path) + +def install(): + pkg.install() + +def build(): + pkg.install() + pkg.run_script('build') \ No newline at end of file diff --git a/openflexure_microscope/lock.py b/openflexure_microscope/lock.py deleted file mode 100644 index 59a37a32..00000000 --- a/openflexure_microscope/lock.py +++ /dev/null @@ -1,74 +0,0 @@ -from threading import RLock - -from openflexure_microscope.exceptions import LockError - - -class StrictLock(object): - """ - Class that behaves like a Python RLock, but with stricter timeout conditions and custom exceptions. - - Args: - timeout (int): Time, in seconds, lock acquisition will wait before raising an exception - - Attributes: - _lock (:py:class:`threading.RLock`): Parent RLock object - timeout (int): Time, in seconds, lock acquisition will wait before raising an exception - """ - def __init__(self, timeout=1): - self._lock = RLock() - self.timeout = timeout - - def locked(self): - return self._lock.locked() - - def acquire(self, blocking=True): - return self._lock.acquire(blocking, timeout=self.timeout) - - def __enter__(self): - result = self._lock.acquire(blocking=True, timeout=self.timeout) - if result: - return result - else: - raise LockError('ACQUIRE_ERROR', self) - - def __exit__(self, *args): - self._lock.release() - - def release(self): - self._lock.release() - - -class CompositeLock(object): - """ - Class that behaves like a :py:class:`openflexure_microscope.lock.StrictLock`, - but allows multiple locks to be acquired and released. - - Args: - locks (list): List of parent RLock objects - timeout (int): Time, in seconds, lock acquisition will wait before raising an exception - - Attributes: - locks (list): List of parent RLock objects - timeout (int): Time, in seconds, lock acquisition will wait before raising an exception - """ - def __init__(self, locks, timeout=1): - self.locks = locks - self.timeout = timeout - - def acquire(self, blocking=True): - return (lock.acquire(blocking=blocking) for lock in self.locks) - - def __enter__(self): - result = (lock.acquire(blocking=True) for lock in self.locks) - if all(result): - return result - else: - raise LockError('ACQUIRE_ERROR', self) - - def __exit__(self, *args): - for lock in self.locks: - lock.release() - - def release(self): - for lock in self.locks: - lock.release() diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 874e5282..637d518c 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -5,59 +5,64 @@ Defines a microscope object, binding a camera and stage with basic functionality import logging import pkg_resources import uuid +from typing import Tuple +from expiringdict import ExpiringDict -from .stage.base import BaseStage +from openflexure_microscope.captures import CaptureManager -from .camera.base import BaseCamera +from openflexure_microscope.stage.mock import MissingStage +from openflexure_microscope.camera.mock import MissingCamera +from openflexure_microscope.stage.sanga import SangaStage -from .plugins import PluginMount -from .utilities import axes_to_array -from .task import TaskOrchestrator -from .lock import CompositeLock -from .config import OpenflexureSettingsFile, settings_to_json +try: + from openflexure_microscope.camera.pi import PiCameraStreamer +except Exception as e: + logging.error(e) + logging.warning("Unable to import PiCameraStreamer") +from openflexure_microscope.camera.mock import MissingCamera + +from openflexure_microscope.utilities import serialise_array_b64, Timer +from openflexure_microscope.config import user_settings, user_configuration + +from labthings import CompositeLock class Microscope: """ A basic microscope object. - The camera and stage should already be initialised, and passed as arguments. - - Attributes: - settings_file (:py:class:`openflexure_microscope.config.OpenflexureSettingsFile`): Runtime-config object, - automatically created if None. - lock (:py:class:`openflexure_microscope.lock.CompositeLock`): Composite lock controlling thread access - to multiple pieces of hardware. - camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): Camera object - stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object - task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing - background tasks using microscope hardware - plugin (:py:class:`openflexure_microscope.plugins.PluginMount`): Mounting point for all microscope plugins + The camera and stage objects may already be initialised, and can be passed as arguments. """ - def __init__(self): - # Initial attributes - self.id = uuid.uuid4().hex + def __init__(self, settings=user_settings, configuration=user_configuration): + self.id = f"openflexure:microscope:{uuid.uuid4()}" self.name = self.id - self.fov = [0, 0] - self.plugin_maps = [] - self.camera = None - self.stage = None + + self.captures = CaptureManager() + + self.fov = [0, 0] #: Microscope field-of-view in stage motor steps + + # Store settings and configuration files + self.settings_file = settings + self.configuration_file = configuration + + self.extension_settings = {} # Initialise with an empty composite lock + #: :py:class:`labthings.CompositeLock`: Composite lock for locking both camera and stage self.lock = CompositeLock([]) - # Create a task orchestrator - self.task = TaskOrchestrator() + self.camera = None #: Currently connected camera object + self.stage = None #: Currently connected stage object + + self.setup(self.configuration_file.load()) # Attach components - # Attach to a settings file - self.settings_file = OpenflexureSettingsFile(expand=True) # Apply settings loaded from file - self.apply_config(self.settings_file.load()) + self.update_settings(self.settings_file.load()) - # Create plugin mount-point and attach plugins from maps - self.plugin = PluginMount(self) - self.attach_plugins(self.plugin_maps) + # Data cache + self.configuration_cache = ExpiringDict(max_len=100, max_age_seconds=3600) + self.metadata_cache = ExpiringDict(max_len=100, max_age_seconds=3600) def __enter__(self): """Create microscope on context enter.""" @@ -71,73 +76,77 @@ class Microscope: """Shut down the microscope hardware.""" logging.info("Closing {}".format(self)) if self.camera: - self.camera.close() + try: + self.camera.close() + except TimeoutError as e: + logging.error(e) if self.stage: - self.stage.close() + try: + self.stage.close() + except TimeoutError as e: + logging.error(e) + self.captures.close() logging.info("Closed {}".format(self)) - def attach(self, camera: BaseCamera, stage: BaseStage): + def setup(self, configuration): """ - Retroactively attaches a camera and stage to the microscope object. - - Allows the microscope to be created as a "dummy", with hardware communications - opened at a later time. - - Args: - camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): camera object - stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object + Attach microscope components based on initially passed configuration file """ - settings_full = self.read_config() + ### Detector + logging.info("Creating camera") + if configuration.get("camera"): + camera_type = configuration["camera"].get("type") + if camera_type in ("PiCamera", "PiCameraStreamer"): + try: + self.camera = PiCameraStreamer() + except Exception as e: + logging.error(e) + logging.warning("No compatible camera hardware found.") - # TODO: Actually attach dummy hardware! - # Maybe even attach dummy hardware at __init__, and replace with real hardware if it exists + ### Stage + logging.info("Creating stage") + if configuration.get("stage"): + stage_type = configuration["stage"].get("type") + stage_port = configuration["stage"].get("port") + if stage_type in ("SangaBoard", "SangaStage"): + try: + self.stage = SangaStage(port=stage_port) + except Exception as e: + logging.error(e) + logging.warning("No compatible Sangaboard hardware found.") - logging.debug("Attaching camera...") - self.camera = camera #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object + logging.info("Handling fallbacks") + ### Fallbacks if not self.camera: - logging.info("No camera attached.") - else: - logging.info("Attached camera {}".format(camera)) - - if hasattr(self.camera, 'lock'): # If camera has a lock - logging.info("Attaching {} to composite lock.".format(self.camera.lock)) - # Add the lock to the microscope composite lock - self.lock.locks.append(self.camera.lock) - - logging.debug("Attaching stage...") - self.stage = stage #: :py:class:`openflexure_microscope.stage.base.BaseStage`: OpenFlexure stage object + self.camera = MissingCamera() if not self.stage: - logging.info("No stage attached.") + self.stage = MissingStage() + + ### Locks + logging.info("Creating locks") + if hasattr(self.camera, "lock"): + self.lock.locks.append(self.camera.lock) + if hasattr(self.stage, "lock"): + self.lock.locks.append(self.stage.lock) + + def has_real_stage(self) -> bool: + """ + Check if a real (non-mock) stage is currently attached. + """ + if hasattr(self, "stage") and not isinstance(self.stage, MissingStage): + return True else: - logging.info("Attached stage {}".format(stage)) + return False - if hasattr(self.stage, 'lock'): # If stage object has a lock - logging.info("Attaching lock {} to composite lock.".format(self.stage.lock)) - # Add the lock to the microscope composite lock - self.lock.locks.append(self.stage.lock) - - logging.info("Reapplying settings to newly attached devices") - self.apply_config(settings_full) - - def attach_plugins(self, plugin_maps: list): + def has_real_camera(self): """ - Automatically search for plugin maps in config, and attach. + Check if a real (non-mock) camera is currently attached. """ - if plugin_maps: - for plugin_map in plugin_maps: - self.plugin.attach(plugin_map) + if hasattr(self, "camera") and not isinstance(self.camera, MissingCamera): + return True else: - logging.warning("No plugins specified. Skipping.") - - def reload_plugins(self): - """ - Empty the plugin mount and re-attach from config. - """ - logging.info("Tearing down existing PluginMount...") - self.plugin = PluginMount(self) - logging.info("Repopulating PluginMount...") - self.attach_plugins(self.plugin_maps) + return False # Create unified state @property @@ -145,42 +154,45 @@ class Microscope: """Dictionary of the basic microscope state. Return: - dict: Dictionary containing position data, - and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state` + dict: Dictionary containing complete microscope state """ - state = { - 'camera': self.camera.state, - 'stage': self.stage.state, - 'plugin': self.plugin.state, - 'version': pkg_resources.get_distribution('openflexure_microscope').version - } - return state + with self.lock: + state = {"camera": self.camera.state, "stage": self.stage.state} + return state - def apply_config(self, config: dict): + def update_settings(self, settings: dict): """ - Applies a config dictionary. Missing parameters will be left untouched. + Applies a settings dictionary to the microscope. Missing parameters will be left untouched. """ - logging.debug("Microscope: Applying config: {}".format(config)) + with self.lock: + logging.debug("Microscope: Applying settings: {}".format(settings)) - # If attached to a camera - if ('camera_settings' in config) and self.camera: - self.camera.apply_config(config['camera_settings']) + # If attached to a camera + if ("camera" in settings) and self.camera: + self.camera.update_settings(settings.get("camera", {})) - # If attached to a stage - if ('stage_settings' in config) and self.stage: - self.stage.apply_config(config['stage_settings']) + # If attached to a stage + if ("stage" in settings) and self.stage: + self.stage.update_settings(settings.get("stage", {})) - # Todo: tidy up with some loopy goodness - if 'id' in config: - self.id = config['id'] - if 'name' in config: - self.name = config['name'] - if 'fov' in config: - self.fov = config['fov'] - if 'plugins' in config: - self.plugin_maps = config['plugins'] + # Capture manager + self.captures.update_settings(settings.get("captures", {})) - def read_config(self, json_safe=False): + # Microscope settings + if "id" in settings: + self.id = settings["id"] + if "name" in settings: + self.name = settings["name"] + if "fov" in settings: + self.fov = settings["fov"] + + # Extension settings + if "extensions" in settings: + self.extension_settings.update(settings["extensions"]) + + # TODO: warn if there are settings that we silently ignore + + def read_settings(self, full: bool = True): """ Get an updated settings dictionary. @@ -190,54 +202,168 @@ class Microscope: This is to ensure that settings for currently disconnected hardware don't get removed from the settings file. """ - settings_current = { - 'id': self.id, - 'name': self.name, - 'fov': self.fov, - 'plugins': self.plugin_maps + "id": self.id, + "name": self.name, + "fov": self.fov, + "extensions": self.extension_settings, } - # If attached to a camera - if self.camera: - settings_current_camera = self.camera.read_config() - settings_current['camera_settings'] = settings_current_camera + with self.lock: + # If attached to a camera + if self.camera: + settings_current_camera = self.camera.read_settings() + settings_current["camera"] = settings_current_camera - # If attached to a stage - if self.stage: - settings_current_stage = self.stage.read_config() - settings_current['stage_settings'] = settings_current_stage + # If attached to a stage + if self.stage: + settings_current_stage = self.stage.read_settings() + settings_current["stage"] = settings_current_stage - settings_full = self.settings_file.merge(settings_current) + # Capture manager + settings_current_captures = self.captures.read_settings() + settings_current["captures"] = settings_current_captures - if json_safe: - settings_full = settings_to_json(settings_full, clean_keys=True) + settings_full = self.settings_file.merge(settings_current) - return settings_full + if full: + return settings_full + else: + return settings_current - def save_config(self, backup: bool = True): + def save_settings(self): """ Merges the current settings back to disk """ # Read curent config - current_config = self.read_config() - # Merge in server version responsible for saving the config file - current_config['server_version'] = pkg_resources.get_distribution('openflexure_microscope').version + current_config = self.read_settings() # Save config to file self.settings_file.save(current_config, backup=True) - @property - def config(self) -> dict: - logging.warn( - "Reading microscope through config property is deprecated.\ - Please use read_config method instead." - ) - return self.read_config() + def force_get_configuration(self): + with self.lock: + initial_configuration = self.configuration_file.load() - @config.setter - def config(self, config: dict) -> None: - logging.warn( - "Setting microscope through config property is deprecated.\ - Please use apply_config method instead." - ) - self.apply_config(config) + current_configuration = { + "application": { + "name": "openflexure-microscope-server", + "version": pkg_resources.get_distribution( + "openflexure-microscope-server" + ).version, + }, + "stage": { + "type": self.stage.__class__.__name__, + **self.stage.configuration, + }, + "camera": { + "type": self.camera.__class__.__name__, + **self.camera.configuration, + }, + } + + initial_configuration.update(current_configuration) + return initial_configuration + + def get_configuration(self, cache_key=None): + if cache_key: + cached_config = self.configuration_cache.get(cache_key, None) + if cached_config: + return cached_config + else: + full_config = self.force_get_configuration() + self.configuration_cache[cache_key] = full_config + return full_config + return self.force_get_configuration() + + @property + def configuration(self): + return self.get_configuration() + + def force_get_metadata(self): + """ + Read cachable bits of microscope metadata. + Currently ID, settings, and configuration can be cached + """ + system_metadata = { + "id": self.id, + "settings": self.read_settings(full=False), + "configuration": self.get_configuration(), + } + + return system_metadata + + def get_metadata(self, cache_key=None): + """ + Read microscope metadata, with partial caching + """ + metadata = {} + + # Load cached bits of metadata + if cache_key: + logging.debug(f"Reading cached microscope metadata: {cache_key}") + metadata = self.metadata_cache.get(cache_key, None) + if not metadata: + logging.debug(f"Building and caching microscope metadata: {cache_key}") + metadata = self.force_get_metadata() + self.metadata_cache[cache_key] = metadata + else: + logging.debug(f"Building microscope metadata: {cache_key}") + metadata = self.force_get_metadata() + + # Keys that should never be cached + metadata.update({ + "state": self.state, + }) + + return metadata + + @property + def metadata(self): + return self.get_metadata() + + def capture( + self, + filename: str = None, + folder: str = "", + temporary: bool = False, + use_video_port: bool = False, + resize: Tuple[int, int] = None, + bayer: bool = True, + fmt: str = "jpeg", + annotations: dict = None, + tags: list = None, + metadata: dict = None, + cache_key: str = None + ): + logging.debug(f"Microscope capturing to {filename}") + if not annotations: + annotations = {} + if not metadata: + metadata = {} + if not tags: + tags = [] + + # Read metadata for capture + full_metadata = {"instrument": self.get_metadata(cache_key), **metadata} + + # Do capture + with self.camera.lock: + # Create output object + output = self.captures.new_image( + temporary=temporary, filename=filename, folder=folder, fmt=fmt + ) + + # Capture to output object + logging.info(f"Starting microscope capture {filename}") + self.camera.capture( + output, + use_video_port=use_video_port, + resize=resize, + bayer=bayer, + fmt=fmt, + ) + + output.put_and_save(tags, annotations, full_metadata) + logging.debug(f"Finished capture to {output.file}") + + return output diff --git a/openflexure_microscope/microscope_configuration.default.json b/openflexure_microscope/microscope_configuration.default.json new file mode 100644 index 00000000..b9cbfdce --- /dev/null +++ b/openflexure_microscope/microscope_configuration.default.json @@ -0,0 +1,9 @@ +{ + "camera": { + "type": "PiCamera" + }, + "stage": { + "type": "SangaStage", + "port": null + } +} \ No newline at end of file diff --git a/openflexure_microscope/microscope_settings.default.json b/openflexure_microscope/microscope_settings.default.json new file mode 100644 index 00000000..5c408d2e --- /dev/null +++ b/openflexure_microscope/microscope_settings.default.json @@ -0,0 +1,7 @@ +{ + "fov": [ + 4100, + 3146 + ], + "jpeg_quality": 100 +} \ No newline at end of file diff --git a/openflexure_microscope/microscope_settings.default.yaml b/openflexure_microscope/microscope_settings.default.yaml deleted file mode 100644 index 4d564043..00000000 --- a/openflexure_microscope/microscope_settings.default.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Field of view, in stage steps -fov: [4100, 3146] - -# Capture quality -jpeg_quality: 75 - -# Parameters specific to camera objects -camera_settings: ~/.openflexure/camera_settings.yaml - -# Parameters specific to stage objects -stage_settings: ~/.openflexure/stage_settings.yaml - -# Default plugins -plugins: - - openflexure_microscope.plugins.default.autofocus:AutofocusPlugin - - openflexure_microscope.plugins.default.scan:ScanPlugin - - openflexure_microscope.plugins.default.camera_calibration:Plugin \ No newline at end of file diff --git a/openflexure_microscope/paths.py b/openflexure_microscope/paths.py new file mode 100644 index 00000000..b9d26bda --- /dev/null +++ b/openflexure_microscope/paths.py @@ -0,0 +1,84 @@ +import os +import logging + +# UTILITIES + + +def check_rw(path): + return os.access(path, os.W_OK) and os.access(path, os.R_OK) + + +def settings_file_path(filename: str): + """Generate a full file path for a filename to be stored in server settings folder""" + settings_dir = os.path.join(OPENFLEXURE_VAR_PATH, "settings") + if not os.path.exists(settings_dir): + os.makedirs(settings_dir) + return os.path.join(settings_dir, filename) + + +def data_file_path(filename: str): + """Generate a full file path for a filename to be stored in server data folder""" + data_dir = os.path.join(OPENFLEXURE_VAR_PATH, "data") + if not os.path.exists(data_dir): + os.makedirs(data_dir) + return os.path.join(data_dir, filename) + + +def extensions_file_path(filename: str): + """Generate a full file path for a folder to be stored in server extensions""" + ext_dir = os.path.join(OPENFLEXURE_VAR_PATH, "extensions") + if not os.path.exists(ext_dir): + os.makedirs(ext_dir) + return os.path.join(ext_dir, filename) + + +def logs_file_path(filename: str): + """Generate a full file path for a filename to be stored in server logs""" + logs_dir = os.path.join(OPENFLEXURE_VAR_PATH, "logs") + if not os.path.exists(logs_dir): + os.makedirs(logs_dir) + return os.path.join(logs_dir, filename) + + +# HANDLE DEFAULTS FILES STORED IN THIS APPLICATION + +HERE = os.path.abspath(os.path.dirname(__file__)) + +#: Path of default (first-run) microscope settings +DEFAULT_SETTINGS_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json") +#: Path of default (first-run) microscope configuration +DEFAULT_CONFIGURATION_FILE_PATH = os.path.join( + HERE, "microscope_configuration.default.json" +) + +# BASE PATHS + +if os.name == "nt": + PREFERRED_VAR_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData" + FALLBACK_VAR_PATH = os.path.expanduser("~") +else: + PREFERRED_VAR_PATH = "/var" + FALLBACK_VAR_PATH = os.path.expanduser("~") + +PREFERRED_OPENFLEXURE_VAR_PATH = os.path.join(PREFERRED_VAR_PATH, "openflexure") +FALLBACK_OPENFLEXURE_VAR_PATH = os.path.join(FALLBACK_VAR_PATH, "openflexure") + +if not os.path.exists(PREFERRED_OPENFLEXURE_VAR_PATH) and check_rw(PREFERRED_VAR_PATH): + os.makedirs(PREFERRED_OPENFLEXURE_VAR_PATH) + +if check_rw(PREFERRED_OPENFLEXURE_VAR_PATH): + OPENFLEXURE_VAR_PATH = PREFERRED_OPENFLEXURE_VAR_PATH +else: + if not os.path.exists(FALLBACK_OPENFLEXURE_VAR_PATH): + os.makedirs(FALLBACK_OPENFLEXURE_VAR_PATH) + OPENFLEXURE_VAR_PATH = FALLBACK_OPENFLEXURE_VAR_PATH + + +# SERVER PATHS + +#: Path of microscope settings file +SETTINGS_FILE_PATH = settings_file_path("microscope_settings.json") +#: Path of microscope configuration file +CONFIGURATION_FILE_PATH = settings_file_path("microscope_configuration.json") +#: Path of microscope extensions directory +OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions") diff --git a/openflexure_microscope/plugins/__init__.py b/openflexure_microscope/plugins/__init__.py deleted file mode 100644 index b335100a..00000000 --- a/openflexure_microscope/plugins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ['module_from_file', 'load_plugin_class', 'load_plugin_module', 'class_from_map', 'PluginMount', 'MicroscopePlugin'] - -from .loader import module_from_file, load_plugin_class, load_plugin_module, class_from_map, PluginMount, MicroscopePlugin \ No newline at end of file diff --git a/openflexure_microscope/plugins/default/autofocus/__init__.py b/openflexure_microscope/plugins/default/autofocus/__init__.py deleted file mode 100644 index 63a64300..00000000 --- a/openflexure_microscope/plugins/default/autofocus/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__all__ = ['AutofocusPlugin'] -from .plugin import AutofocusPlugin \ No newline at end of file diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py deleted file mode 100644 index 5f598449..00000000 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ /dev/null @@ -1,39 +0,0 @@ -import numpy as np -import logging - -from openflexure_microscope.devel import MicroscopeViewPlugin, JsonPayload, request, jsonify - -class MeasureSharpnessAPI(MicroscopeViewPlugin): - def post(self): - payload = JsonPayload(request) - return jsonify({'sharpness': self.plugin.measure_sharpness()}) - - -class AutofocusAPI(MicroscopeViewPlugin): - def post(self): - payload = JsonPayload(request) - - # Figure out the range of z values to use - dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array) - - logging.info("Running autofocus...") - task = self.microscope.task.start(self.plugin.autofocus, dz) - - # return a handle on the autofocus task - return jsonify(task.state), 202 - -class FastAutofocusAPI(MicroscopeViewPlugin): - def post(self): - payload = JsonPayload(request) - - # Figure out the parameters to use - dz = payload.param("dz", default=2000, convert=int) - backlash = payload.param("backlash", default=0, convert=int) - if backlash < 0: - backlash = 0 - - logging.info("Running autofocus...") - task = self.microscope.task.start(self.plugin.fast_autofocus, dz, backlash=backlash) - - # return a handle on the autofocus task - return jsonify(task.state), 202 \ No newline at end of file diff --git a/openflexure_microscope/plugins/default/autofocus/focus_utils.py b/openflexure_microscope/plugins/default/autofocus/focus_utils.py deleted file mode 100644 index 814399a3..00000000 --- a/openflexure_microscope/plugins/default/autofocus/focus_utils.py +++ /dev/null @@ -1,122 +0,0 @@ -import time -import numpy as np -import threading -import logging -from scipy import ndimage - -class JPEGSharpnessMonitor(): - def __init__(self, microscope, timeout=60): - self.microscope = microscope - self.camera = microscope.camera - self.stage = microscope.stage - self.jpeg_sizes = [] - self.jpeg_times = [] - self.stage_positions = [] - self.stage_times = [] - self.stop_event = threading.Event() - self.timeout = timeout - self.keep_alive() - self.background_thread = None - - def is_alive(self): - if self.background_thread is None: - return False - else: - return self.background_thread.is_alive() - - def should_stop(self): - import time - return time.time() - self.kept_alive > self.timeout - - def keep_alive(self): - import time - self.kept_alive = time.time() - - def start(self): - "Start monitoring sharpness by looking at JPEG size" - self.background_thread = threading.Thread(target=self._measure_jpegs) - self.background_thread.start() - return self - - def stop(self): - "Stop the background thread" - self.stop_event.set() - self.background_thread.join() - - def _measure_jpegs(self): - "Function that runs in a background thread to record sharpness" - logging.info("Starting sharpness measurement in background thread") - self.keep_alive() - while not self.stop_event.is_set() and not self.should_stop(): - self.jpeg_sizes.append(self.jpeg_size()) - self.jpeg_times.append(time.time()) - if self.stop_event.is_set(): - logging.info("Cleanly stopped sharpness measurement in background thread") - if self.should_stop(): - logging.info("Sharpness measurement timed out and has stopped") - - def jpeg_size(self): - """Return the size of a frame from the MJPEG stream""" - return len(self.camera.get_frame()) - - - def focus_rel(self, dz, backlash=False, **kwargs): - self.keep_alive() - self.stage_times.append(time.time()) - self.stage_positions.append(self.stage.position) - self.stage.move_rel([0, 0, dz], backlash=backlash, **kwargs) - self.stage_times.append(time.time()) - self.stage_positions.append(self.stage.position) - i = len(self.stage_positions) - 2 - return i, self.stage_positions[-1][2] - - def move_data(self, istart, istop=None): - "Extract sharpness as a function of (interpolated) z" - global np, logging - if istop is None: - istop = istart + 2 - jpeg_times = np.array(self.jpeg_times) - jpeg_sizes = np.array(self.jpeg_sizes) - stage_times = np.array(self.stage_times)[istart:istop] - stage_zs = np.array(self.stage_positions)[istart:istop, 2] - start = np.argmax(jpeg_times > stage_times[0]) - stop = np.argmax(jpeg_times > stage_times[1]) - if stop < 1: - stop = len(jpeg_times) - logging.debug("changing stop to {}".format(stop)) - jpeg_times = jpeg_times[start:stop] - jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs) - return jpeg_times, jpeg_zs, jpeg_sizes[start:stop] - - def sharpest_z_on_move(self, index): - """Return the z position of the sharpest image on a given move""" - jt, jz, js = self.move_data(index) - return jz[np.argmax(js)] - - def data_dict(self): - """Return the gathered data as a single convenient dictionary""" - data = {} - for k in ['jpeg_times', 'jpeg_sizes', 'stage_times', 'stage_positions']: - data[k] = getattr(self, k) - return data - -def decimate_to(shape, image): - """Decimate an image to reduce its size if it's too big.""" - decimation = np.max(np.ceil(np.array(image.shape, dtype=np.float)[:len(shape)]/np.array(shape))) - return image[::int(decimation), ::int(decimation), ...] - - -def sharpness_sum_lap2(rgb_image): - """Return an image sharpness metric: sum(laplacian(image)**")""" - # image_bw=np.mean(decimate_to((1000,1000), rgb_image),2) - image_bw = np.mean(rgb_image, 2) - image_lap = ndimage.filters.laplace(image_bw) - return np.mean(image_lap.astype(np.float)**4) - - -def sharpness_edge(image): - """Return a sharpness metric optimised for vertical lines""" - gray = np.mean(image.astype(float), 2) - n = 20 - edge = np.array([[-1]*n + [1]*n]) - return np.sum([np.sum(ndimage.filters.convolve(gray, W)**2) for W in [edge, edge.T]]) diff --git a/openflexure_microscope/plugins/default/autofocus/plugin.py b/openflexure_microscope/plugins/default/autofocus/plugin.py deleted file mode 100644 index 9dcd8b30..00000000 --- a/openflexure_microscope/plugins/default/autofocus/plugin.py +++ /dev/null @@ -1,155 +0,0 @@ -import time -import logging -import numpy as np -from contextlib import contextmanager - -from openflexure_microscope.utilities import set_properties - -from .focus_utils import sharpness_sum_lap2, JPEGSharpnessMonitor -from .api import MeasureSharpnessAPI, AutofocusAPI, FastAutofocusAPI - -from openflexure_microscope.devel import MicroscopePlugin - -class AutofocusPlugin(MicroscopePlugin): - """ - Basic autofocus plugin - """ - - api_views = { - '/measure_sharpness': MeasureSharpnessAPI, - '/autofocus': AutofocusAPI, - '/fast_autofocus': FastAutofocusAPI, - } - - ### SLOW AUTOFOCUS - - def autofocus(self, dz, settle=0.5, metric_fn=sharpness_sum_lap2): - """Perform a simple autofocus routine. - The stage is moved to z positions (relative to current position) in dz, - and at each position an image is captured and the sharpness function - evaulated. We then move back to the position where the sharpness was - highest. No interpolation is performed. - dz is assumed to be in ascending order (starting at -ve values) - """ - camera = self.microscope.camera - stage = self.microscope.stage - - with set_properties(stage, backlash=256), stage.lock, camera.lock: - sharpnesses = [] - positions = [] - camera.annotate_text = "" - - for _ in stage.scan_z(dz, return_to_start=False): - positions.append(stage.position[2]) - time.sleep(settle) - sharpnesses.append(self.measure_sharpness(metric_fn)) - - newposition = positions[np.argmax(sharpnesses)] - stage.move_rel([0, 0, newposition - stage.position[2]]) - - return positions, sharpnesses - - def measure_sharpness(self, metric_fn=sharpness_sum_lap2): - """Measure the sharpness of the camera's current view.""" - return metric_fn(self.microscope.camera.array(use_video_port=True)) - - ### FAST AUTOFOCUS - - @contextmanager - def monitor_sharpness(self): - m = JPEGSharpnessMonitor(self.microscope) - m.start() - try: - yield m - finally: - m.stop() - - def move_and_find_focus(self, dz): - """Make a relative Z move and return the peak sharpness position""" - with self.monitor_sharpness() as m: - m.focus_rel(dz) - return m.sharpest_z_on_move(0) - - - def fast_autofocus(self, dz=2000, backlash=None): - """Perform a down-up-down-up autofocus""" - with self.monitor_sharpness() as m: - i, z = m.focus_rel(-dz/2) - i, z = m.focus_rel(dz) - fz = m.sharpest_z_on_move(i) - if backlash is None: - i, z = m.focus_rel(-dz) # move all the way to the start so it's consistent - else: - i, z = m.focus_rel(fz - z - backlash) - m.focus_rel(fz - z) - return m.data_dict() - - def fast_up_down_up_autofocus(self, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150): - """Autofocus by measuring on the way down, and moving back up with feedback. - - This autofocus method is very efficient, as it only passes the peak once. - The sequence of moves it performs is: - - 1. Move to the top of the range `dz/2` (can be disabled) - - 2. Move down by `dz` while monitoring JPEG size to find the focus. - - 3. Move back up to the `target_z` position, relative to the sharpest image. - - 4. Measure the sharpness, and compare against the curve recorded in (2) to \\ - estimate how much further we need to go. Make this move, to reach our \\ - target position. - - Moving back to the target position in two steps allows us to correct for - backlash, by using the sharpness-vs-z curve as a rough encoder for Z. - - Parameters: - dz: number of steps over which to scan (optional, default 2000) - - target_z: we aim to finish at this position, relative to focus. This may - be useful if, for example, you want to acquire a stack of images in Z. - It is optional, and the default value of 0 will finish at the focus. - - initial_move_up: (optional, default True) set this to `False` to move down - from the starting position. Mostly useful if you're able to combine - the initial move with something else, e.g. moving to the next scan point. - - mini_backlash: (optional, default 50) is a small extra move made in step - 3 to help counteract backlash. It should be small enough that you - would always expect there to be greater backlash than this. Too small - might slightly hurt accuracy, but is unlikely to be a big issue. Too big - may cause you to overshoot, which is a problem. - """ - with self.monitor_sharpness() as m, self.microscope.camera.lock: - # Ensure the MJPEG stream has started - self.microscope.camera.start_stream_recording() - - df = dz #TODO: refactor so I actually use dz in the code below! - if initial_move_up: - m.focus_rel(df/2) - # move down - i, z = m.focus_rel(-df) - # now inspect where the sharpest point is, and estimate the sharpness - # (JPEG size) that we should find at the start of the Z stack - jt, jz, js = m.move_data(i) - best_z = jz[np.argmax(js)] - target_s = np.interp([best_z+target_z], jz[::-1], js[::-1]) #NB jz is decreasing - - # now move to the start of the z stack - i, z = m.focus_rel(best_z + target_z - z + mini_backlash) # takes us to the start of the stack - - # We've deliberately undershot - figure out how much further we should move based on the curve - current_js = m.jpeg_size() - imax = np.argmax(js) # we want to crop out just the bit below the peak - js = js[imax:] # NB z is in DECREASING order - jz = jz[imax:] - inow = np.argmax(js < current_js) # use the curve we recorded to estimate our position - # TODO: fancy interpolation stuff - - # So, the Z position corresponding to our current sharpness value is zs[inow] - # That means we should move forwards, by best_z - zs[inow] - correction_move = best_z + target_z - jz[inow] - logging.debug("Fast autofocus scan: correcting backlash by moving {} steps".format(correction_move)) - m.focus_rel(correction_move) - return m.data_dict() - diff --git a/openflexure_microscope/plugins/default/camera_calibration/__init__.py b/openflexure_microscope/plugins/default/camera_calibration/__init__.py deleted file mode 100644 index 0f02ba77..00000000 --- a/openflexure_microscope/plugins/default/camera_calibration/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__all__ = ['Plugin'] -from .plugin import Plugin diff --git a/openflexure_microscope/plugins/default/camera_calibration/plugin.py b/openflexure_microscope/plugins/default/camera_calibration/plugin.py deleted file mode 100644 index e57a4a47..00000000 --- a/openflexure_microscope/plugins/default/camera_calibration/plugin.py +++ /dev/null @@ -1,50 +0,0 @@ -from openflexure_microscope.devel import MicroscopePlugin, MicroscopeViewPlugin, JsonPayload, request, jsonify - -import logging - -from .recalibrate_utils import recalibrate_camera, auto_expose_and_freeze_settings - - -class RecalibrateAPIView(MicroscopeViewPlugin): - def post(self): - logging.info("Starting microscope recalibration...") - task = self.microscope.task.start(self.plugin.recalibrate) - - # Return a handle on the autofocus task - return jsonify(task.state), 202 - - -class Plugin(MicroscopePlugin): - """ - A set of default plugins - """ - - api_views = { - '/recalibrate': RecalibrateAPIView, - } - - def recalibrate(self): - """Reset the camera's settings. - - This generates new gains, exposure time, and lens shading - table such that the background is as uniform as possible - with a gray level of 230. It takes a little while to run. - """ - scamera = self.microscope.camera - with scamera.lock: - assert not scamera.state['record_active'], "Can't recalibrate while recording!" - streaming = scamera.state['stream_active'] - if streaming: - logging.info("Stopping stream before recalibration") - scamera.stop_stream_recording(resolution=(640, 480)) - old_resolution = scamera.camera.resolution - try: - scamera.camera.resolution = (640, 480) - auto_expose_and_freeze_settings(scamera.camera) - recalibrate_camera(scamera.camera) - finally: - scamera.camera.resolution = old_resolution - self.microscope.save_config() - if streaming: - logging.info("Restarting stream after recalibration") - scamera.start_stream_recording() diff --git a/openflexure_microscope/plugins/default/scan/__init__.py b/openflexure_microscope/plugins/default/scan/__init__.py deleted file mode 100644 index cfdd7ee4..00000000 --- a/openflexure_microscope/plugins/default/scan/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__all__ = ['ScanPlugin'] -from .plugin import ScanPlugin diff --git a/openflexure_microscope/plugins/default/scan/api.py b/openflexure_microscope/plugins/default/scan/api.py deleted file mode 100644 index 9e8981e5..00000000 --- a/openflexure_microscope/plugins/default/scan/api.py +++ /dev/null @@ -1,53 +0,0 @@ -from openflexure_microscope.devel import MicroscopeViewPlugin, JsonPayload, request, jsonify, abort - -import logging - -class TileScanAPI(MicroscopeViewPlugin): - def post(self): - payload = JsonPayload(request) - - # Get params - filename = payload.param('filename') - temporary = payload.param('temporary', default=False, convert=bool) - - step_size = payload.param('step_size', default=[2000, 1500, 100], convert=list) - step_size = [int(i) for i in step_size] - - grid = payload.param('grid', default=[3, 3, 5], convert=list) - grid = [int(i) for i in grid] - - style = payload.param('style', default='raster', convert=str) - autofocus_dz = payload.param('autofocus_dz', default=50, convert=int) - fast_autofocus = payload.param('fast_autofocus', default=False, convert=bool) - - use_video_port = payload.param('use_video_port', default=True, convert=bool) - resize = payload.param('size', default=None) - if resize: - if ('width' in resize) and ('height' in resize): - resize = (int(resize['width']), int(resize['height'])) # Convert dict to tuple - else: - abort(404) - - bayer = payload.param('bayer', default=False, convert=bool) - metadata = payload.param('metadata', default={}, convert=dict) - tags = payload.param('tags', default=[], convert=list) - - logging.info("Running tile scan...") - task = self.microscope.task.start( - self.plugin.tile, - basename=filename, - temporary=temporary, - step_size=step_size, - grid=grid, - style=style, - autofocus_dz=autofocus_dz, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - fast_autofocus=fast_autofocus, - metadata=metadata, - tags=tags - ) - - # return a handle on the autofocus task - return jsonify(task.state), 202 diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py deleted file mode 100644 index d7f5bceb..00000000 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ /dev/null @@ -1,275 +0,0 @@ -import time -import numpy as np -from typing import Tuple -import uuid -import itertools -import logging - -from openflexure_microscope.camera.base import generate_basename - -from openflexure_microscope.devel import MicroscopePlugin - -from .api import TileScanAPI - -def construct_grid(initial, step_sizes, n_steps, style='raster'): - """ - Given an initial position, step sizes, and number of steps, - construct a 2-dimensional list of scan x-y positions. - """ - arr = [] - - for i in range(n_steps[0]): # x axis - arr.append([]) - for j in range(n_steps[1]): # y axis - # Create a coordinate array - coord = [initial[ax] + [i, j][ax]*step_sizes[ax] for ax in range(2)] - # Append coordinate array to position grid - arr[i].append(tuple(coord)) - - # Style modifiers - if style == 'snake': - for i, line in enumerate(arr): - if i % 2 != 0: - line.reverse() - - return arr - -def flatten_grid(grid): - """ - Convert a 3D list of scan positions into a flat list - of sequential positions. - """ - - grid = list(itertools.chain(*grid)) - return grid - -class ScanPlugin(MicroscopePlugin): - """ - Stack and tile plugin - """ - - api_views = { - '/tile': TileScanAPI, - } - - def capture(self, - basename, - scan_id, - temporary: bool = False, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - metadata: dict = {}, - tags: list = []): - - # Construct a tile filename - filename = "{}_{}_{}_{}".format(basename, *self.microscope.stage.position) - folder = "SCAN_{}".format(basename) - - # Create output object - output = self.microscope.camera.new_image( - write_to_file=True, - temporary=temporary, - filename=filename, - folder=folder) - - # Capture - self.microscope.camera.capture( - output, - use_video_port=use_video_port, - resize=resize, - bayer=bayer) - - # Affix metadata - if 'scan' not in tags: - tags.append('scan') - - metadata.update({ - 'position': self.microscope.state['stage']['position'], - 'scan_id': scan_id, - 'basename': basename, - 'microscope_id': self.microscope.id, - 'microscope_name': self.microscope.name - }) - - output.put_metadata(metadata) - output.put_tags(tags) - - def tile( - self, - basename: str = None, - temporary: bool = False, - step_size: int = [2000, 1500, 100], - grid: list = [3, 3, 5], - style='raster', - autofocus_dz: int = 50, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - fast_autofocus = False, - metadata: dict = {}, - tags: list = []): - - # Generate a basename if none given - if not basename: - basename = generate_basename() - - # Generate a stack ID - scan_id = uuid.uuid4().hex - - # Store initial position - initial_position = self.microscope.stage.position - - # Add scan metadata - if not 'time' in metadata: - metadata['time'] = generate_basename() - - # Check if autofocus is enabled - if autofocus_dz and hasattr(self.microscope.plugin, 'default_autofocus'): - autofocus_enabled = True - else: - autofocus_enabled = False - - if fast_autofocus and not hasattr(self.microscope.plugin.default_autofocus, 'monitor_sharpness'): - logging.error("Can't use fast autofocus in the scan - the default plugin doesn't support monitor_sharpness; maybe it is too old?") - fast_autofocus = False - z_stack_dz = grid[2] * step_size[2] if grid[2] > 1 else 0 # shorthand for Z stack range - - # Construct an x-y grid (worry about z later) - x_y_grid = construct_grid( - initial_position, - step_size[:2], - grid[:2], - style=style - ) - - # Keep the initial Z position the same as our current position - next_z = initial_position[2] - if fast_autofocus: # If fast autofocus is enabled, make - next_z += autofocus_dz/2 # sure we start from the top of the range - initial_z = next_z # Save this value for use in raster scans - - # Now step through each point in the x-y coordinate array - for line in x_y_grid: - # If rastering, rather than snake (or eventually spiral) - # Return focus to initial position - if style == 'raster': - next_z = initial_z - logging.debug("Returning to initial z position") - self.microscope.stage.move_abs([line[0][0], line[0][1], next_z]) #RWB: I think this line is redundant - - for x_y in line: - # Move to new grid position without changing z - logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z])) - self.microscope.stage.move_abs([x_y[0], x_y[1], next_z]) - # Refocus - if autofocus_enabled: - if fast_autofocus: - self.microscope.plugin.default_autofocus.fast_up_down_up_autofocus( - dz=autofocus_dz, - target_z=-z_stack_dz/2.0, # Finish below the focus - initial_move_up=False, # We're already at the top of the scan - ) - #TODO: save the focus data for future reference? Use it for diagnostics? - else: - logging.debug("Running autofocus") - self.microscope.plugin.default_autofocus.autofocus( - range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)) - logging.debug("Finished autofocus") - time.sleep(1) # TODO: Remove - - # If we're not doing a z-stack, just capture - if (grid[2] <= 1): - self.capture( - basename, - scan_id, - temporary=temporary, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - metadata=metadata, - tags=tags - ) - else: - logging.debug("Entering z-stack") - self.stack( - basename=basename, - temporary=temporary, - scan_id=scan_id, - step_size=step_size[2], - steps=grid[2], - center=not fast_autofocus, # fast_autofocus does this for us! - return_to_start=not fast_autofocus, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - metadata=metadata, - tags=tags - ) - # Make sure we use our current best estimate of focus (i.e. the current position) next point - next_z = self.microscope.stage.position[2] - if fast_autofocus: - next_z += autofocus_dz/2 # Fast autofocus requires us to start at the top of the range - if grid[2] > 1: - next_z -= int(grid[2]/2.0*step_size[2]) # Z stacking means we're higher up to start with - - logging.debug("Returning to {}".format(initial_position)) - self.microscope.stage.move_abs(initial_position) - - def stack( - self, - basename: str = None, - temporary: bool = False, - scan_id: str = None, - step_size: int = 100, - steps: int = 5, - center: bool = True, - return_to_start: bool = True, - use_video_port: bool = False, - resize: Tuple[int, int] = None, - bayer: bool = False, - metadata: dict = {}, - tags: list = []): - - # Generate a basename if none given - if not basename: - basename = generate_basename() - - # Generate a stack ID - if not scan_id: - scan_id = uuid.uuid4().hex - - # Add scan metadata - if not 'time' in metadata: - metadata['time'] = generate_basename() - - # Store initial position - initial_position = self.microscope.stage.position - - - with self.microscope.lock: - # Move to center scan - if center: - logging.debug("Moving to starting position") - self.microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)]) - - for i in range(steps): - time.sleep(0.1) - logging.debug("Capturing...") - self.capture( - basename, - scan_id, - temporary=temporary, - use_video_port=use_video_port, - resize=resize, - bayer=bayer, - metadata=metadata, - tags=tags - ) - - if i != steps - 1: - logging.debug("Moving z by {}".format(step_size)) - self.microscope.stage.move_rel([0, 0, step_size]) - if return_to_start: - logging.debug("Returning to {}".format(initial_position)) - self.microscope.stage.move_abs(initial_position) diff --git a/openflexure_microscope/plugins/example/__init__.py b/openflexure_microscope/plugins/example/__init__.py deleted file mode 100644 index b1d5b636..00000000 --- a/openflexure_microscope/plugins/example/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__all__ = ['Plugin', 'HelloWorldAPI', 'IdentifyAPI'] - -from .plugin import Plugin -from .api import HelloWorldAPI, IdentifyAPI \ No newline at end of file diff --git a/openflexure_microscope/plugins/example/api.py b/openflexure_microscope/plugins/example/api.py deleted file mode 100644 index 0afe6dd2..00000000 --- a/openflexure_microscope/plugins/example/api.py +++ /dev/null @@ -1,95 +0,0 @@ -from openflexure_microscope.api.utilities import JsonPayload -from openflexure_microscope.api.v1.views import MicroscopeViewPlugin -from openflexure_microscope.exceptions import TaskDeniedException - -from flask import request, Response, escape, jsonify, abort - - -class IdentifyAPI(MicroscopeViewPlugin): - """ - A simple example API plugin, attached through the main microscope plugin. - """ - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - data = self.plugin.identify() # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut - return Response(escape(data)) - - -class HelloWorldAPI(MicroscopeViewPlugin): - """ - A method to create, set, and return a new microscope parameter. - """ - - def get(self): - """ - Method to call when an HTTP GET request is made. - """ - - # If the microscope does not already contain our plugin_string attribute - if not hasattr(self.microscope, 'plugin_string'): - # Make a string, using the MicroscopeViewPlugin.plugin shortcut - self.microscope.plugin_string = self.plugin.hello_world() - - return Response(self.microscope.plugin_string) - - def post(self): - """ - Method to call when an HTTP POST request is made. - Assumes request will include a JSON payload. - """ - # Get payload JSON - payload = JsonPayload(request) - - # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty. - new_plugin_string = payload.param('plugin_string', default='', convert=str) - - if new_plugin_string: # If not None or empty - # Set microscope attribute to the specified string - self.microscope.plugin_string = new_plugin_string - - return Response(self.microscope.plugin_string) - - -class LongRunningAPI(MicroscopeViewPlugin): - """ - An example API plugin that uses a long-running plugin method. - """ - def post(self): - """ - Method to call when an HTTP POST request is made. - """ - # Get payload JSON - payload = JsonPayload(request) - - # Extract a values from the JSON payload. - time_to_run = payload.param('time', default=10, convert=int) - - # Attach the long-running method as a microscope task - try: - task = self.microscope.task.start(self.plugin.long_running, time_to_run) - return jsonify(task.state), 202 - - except TaskDeniedException: - return abort(409) - - -class SomeExceptionAPI(MicroscopeViewPlugin): - """ - An example API plugin that uses a long-running but broken plugin method. - """ - def post(self): - """ - Method to call when an HTTP POST request is made. - """ - # Get payload JSON - payload = JsonPayload(request) - - # Attach the long-running method as a microscope task - try: - task = self.microscope.task.start(self.plugin.some_exception) - return jsonify(task.state), 202 - - except TaskDeniedException: - return abort(409) diff --git a/openflexure_microscope/plugins/example/plugin.py b/openflexure_microscope/plugins/example/plugin.py deleted file mode 100644 index 9c7335ea..00000000 --- a/openflexure_microscope/plugins/example/plugin.py +++ /dev/null @@ -1,61 +0,0 @@ -import random -import time -from openflexure_microscope.plugins import MicroscopePlugin - -from .api import IdentifyAPI, HelloWorldAPI, LongRunningAPI, SomeExceptionAPI - - -class Plugin(MicroscopePlugin): - """ - A set of default plugins - """ - - api_views = { - '/identify': IdentifyAPI, - '/hello': HelloWorldAPI, - '/long_running': LongRunningAPI, - '/some_exception': SomeExceptionAPI, - } - - def identify(self): - """ - Demonstrate access to Microscope.camera, and Microscope.stage - """ - - response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, self.microscope.stage) - print(response) - return response - - def hello_world(self): - """ - Demonstrate passive method - """ - - return "Hello world!" - - def some_exception(self): - """ - Demonstrate some broken plugin method that would be long running - """ - with self.microscope.lock: - time.sleep(3) - result = 15./0 - - return result - - def long_running(self, t_run): - """ - Demonstrate a long-running method that requires microscope hardware - """ - print("Starting a long-running task...") - n_array = [] - - print("Acquiring composite microscope lock...") - with self.microscope.camera.lock, self.microscope.stage.lock: - for _ in range(t_run): - n_array.append(random.random()) - time.sleep(1) - - print("Long-running task finished! Releasing locks.") - - return n_array diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py deleted file mode 100644 index d484b4ee..00000000 --- a/openflexure_microscope/plugins/loader.py +++ /dev/null @@ -1,198 +0,0 @@ -import importlib -import os -import inspect -import logging - - -class ConColors: - HEADER = '\033[95m' - OKBLUE = '\033[94m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' - - -def module_from_file(plugin_path): - # Expand environment variables in path string - plugin_path = os.path.expandvars(plugin_path) - # Expand user directory in path string - plugin_path = os.path.expanduser(plugin_path) - - # Check if the path is to a file - if not os.path.isfile(plugin_path): - logging.warning(ConColors.FAIL + "No valid plugin found at {}.".format(plugin_path) + ConColors.ENDC) - return None, None, None - - else: - # Get name of plugin from the file - plugin_name = os.path.splitext(os.path.basename(plugin_path))[0] - - plugin_spec = importlib.util.spec_from_file_location(plugin_name, plugin_path) - plugin_module = importlib.util.module_from_spec(plugin_spec) - - return plugin_spec, plugin_module, plugin_name - -def check_module(module_path): - """ - Used to check if a module exists, without ever importing it. - - This checks each nested level separately to avoid raising exceptions. - For example, checking "mymodule.submodule.subsubmodule" will first - check if 'mymodule' exists, then if it does, it will check - 'mymodule.submodule', and so on. - """ - module_split = module_path.split(".") - spec_path = "" - - for module_level in module_split: - spec_path += ".{}".format(module_level) - spec_path = spec_path.strip(".") - - logging.debug("Checking {}".format(spec_path)) - - # Try to find a module loader for the plugin path - plugin_spec = importlib.util.find_spec(spec_path) - - # If plugin spec doesn't exist, return False early - if plugin_spec is None: - return False - - # If all checks pass, return True - return True - - -def name_from_module(plugin_path): - path_array = plugin_path.split('.') - - # Truncate namespace of default plugins - if path_array[0:2] == ['openflexure_microscope', 'plugins']: - path_array = path_array[2:] - - return '/'.join(path_array) - - -def load_plugin_module(plugin_path): - - # If the loader was found (i.e. plugin probably exists) - if check_module(plugin_path): - try: - plugin_module = importlib.import_module(plugin_path) - plugin_name = name_from_module(plugin_path) - except Exception as e: - logging.error("Error loading plugin.") - logging.error(e) - return None, None - - # If no loader was found, try finding a file from path - else: - plugin_spec, plugin_module, plugin_name = module_from_file(plugin_path) - # If a valid plugin was found - if plugin_spec and plugin_module: - # Execute the module, so we have access to it - plugin_spec.loader.exec_module(plugin_module) - - return plugin_module, plugin_name - - -def load_plugin_class(plugin_path, plugin_class_name): - plugin_module, plugin_name = load_plugin_module(plugin_path) - if plugin_module: - # Now try to extract the class - try: - plugin_class = getattr(plugin_module, plugin_class_name) - except AttributeError: - logging.warning(ConColors.FAIL + "Class {} does not exist in plugin {}. Skipping.".format(plugin_class_name, plugin_path) + ConColors.ENDC) - return None, None - else: - return plugin_class, plugin_name - else: - return None, None - - -def class_from_map(plugin_map): - plugin_arr = plugin_map.split(':') - - if not len(plugin_arr) == 2: - logging.warning(ConColors.WARNING + "Malformed plugin map {}. Skipping.".format(plugin_map) + ConColors.ENDC) - return None, None - else: - return load_plugin_class(*plugin_arr) - - -class PluginMount(object): - """ - A mount-point for all loaded plugins. Attaches to a Microscope object. - - Args: - parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to. - """ - def __init__(self, parent): - self.parent = parent - self.plugins = [] # List of plugin objects - self.schemas = [] # List of plugin schemas - logging.info("Creating plugin mount") - - @property - def state(self): - return [m[0] for m in self.members] - - @property - def members(self): - ignores = ['state', 'members', 'attach'] - plugin_array = [] - for obj_name in dir(self): - if not obj_name in ignores and not obj_name[:2] == '__': - obj = getattr(self, obj_name) - if isinstance(obj, MicroscopePlugin): - plugin_members = [member for member in inspect.getmembers(obj) if not member[0][:2] == '__'] - plugin_info = (obj_name, plugin_members) - plugin_array.append(plugin_info) - return plugin_array - - def attach(self, plugin_map): - """ - Attach a MicroscopePlugin instance to the plugin mount. - - Args: - plugin_map (str): A plugin map describing the file or module to load a MicroscopePlugin child from. Maps should be in the format 'module.to.load:ClassName' or '/path/to/file:ClassName'. - """ - plugin_class, plugin_name = class_from_map(plugin_map) - - if plugin_class is not None: - - pythonsafe_plugin_name = plugin_name.replace("/", "_") - - if plugin_class and plugin_name: - plugin_object = plugin_class() - - if hasattr(self, plugin_name): # If a plugin with the same name is already attached. - logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC) - - elif isinstance(plugin_object, MicroscopePlugin): # If plugin_object is an instance of MicroscopePlugin - # Attach plugin_object to the plugin mount - setattr(self, pythonsafe_plugin_name, plugin_object) - self.plugins.append((plugin_name, plugin_object)) - - # Grant plugin access to the hardware - plugin_object.microscope = self.parent - - logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC) - - else: - logging.error("Error loading plugin. Moving on.") - -class MicroscopePlugin: - """ - Parent class for all microscope plugins. - - Initially only defines an empty object for microscope. All plugins - must be an instance of this class to successfully attach to PluginMount. - """ - - api_views = {} # Initially empty dictionary of API views associated with the plugin - - def __init__(self): - self.microscope = None #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object diff --git a/openflexure_microscope/plugins/testing/schema_example/__init__.py b/openflexure_microscope/plugins/testing/schema_example/__init__.py deleted file mode 100644 index 3e9a8c4d..00000000 --- a/openflexure_microscope/plugins/testing/schema_example/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .plugin import ExamplePlugin -from . import api \ No newline at end of file diff --git a/openflexure_microscope/plugins/testing/schema_example/api.py b/openflexure_microscope/plugins/testing/schema_example/api.py deleted file mode 100644 index 04cefe66..00000000 --- a/openflexure_microscope/plugins/testing/schema_example/api.py +++ /dev/null @@ -1,61 +0,0 @@ -from openflexure_microscope.devel import MicroscopeViewPlugin, TaskDeniedException, JsonPayload, Response, request, escape, jsonify - -import logging - -class DoAPI(MicroscopeViewPlugin): - """ - A simple example API plugin - """ - - def get(self): - return jsonify(self.plugin.get_values_dict()) - - def post(self): - # Get payload JSON - payload = JsonPayload(request) - - # Extract a values from the JSON payload. - val_int = payload.param('val_int', default=None, convert=int) - val_str = payload.param('val_str', default=None, convert=str) - val_radio = payload.param('val_radio', default=None) - val_check = payload.param('val_check', default=None) - val_select = payload.param('val_select', default=None) - val_disposable = payload.param('val_disposable', default=None) - - self.plugin.set_values( - val_int, - val_str, - val_radio, - val_check, - val_select, - val_disposable - ) - - print(self.plugin.get_values_dict()) - - return jsonify({ - 'response': 'completed' - }) - -class TaskAPI(MicroscopeViewPlugin): - """ - A task example API plugin - """ - - def get(self): - return jsonify({ - 'run_time': self.plugin.run_time - }) - - def post(self): - # Get payload JSON - payload = JsonPayload(request) - - # Extract a values from the JSON payload. - val_int = payload.param('run_time', default=5, convert=int) - - logging.info("Running task...") - task = self.microscope.task.start(self.plugin.generate_random_numbers_for_a_while, val_int) - - # return a handle on the autofocus task - return jsonify(task.state), 202 \ No newline at end of file diff --git a/openflexure_microscope/plugins/testing/schema_example/plugin.py b/openflexure_microscope/plugins/testing/schema_example/plugin.py deleted file mode 100644 index 0e57fe26..00000000 --- a/openflexure_microscope/plugins/testing/schema_example/plugin.py +++ /dev/null @@ -1,72 +0,0 @@ -import random -import time -import os -import json -from openflexure_microscope.devel import MicroscopePlugin - -from .api import DoAPI, TaskAPI - -HERE = os.path.dirname(os.path.realpath(__file__)) -SCHEMA_PATH = os.path.join(HERE, "schema.json") - -class ExamplePlugin(MicroscopePlugin): - """ - An example plugin using a comprehensive schema - """ - global SCHEMA_PATH - - with open(SCHEMA_PATH, 'r') as sc: - api_schema = json.load(sc) - - api_views = { - '/do': DoAPI, - '/task': TaskAPI - } - - def __init__(self): - self.val_int = 10 - self.val_str = "Hello" - self.val_radio = "First" - self.val_check = ["Foo", "Bar"] - self.val_select = "Most" - self.val_unused = "I'm an unused string, here to confuse the form parsing" - - self.run_time = 5 - - def set_values(self, val_int, val_str, val_radio, val_check, val_select, val_disposable): - """ - Demonstrate a plugin with schema - """ - if val_int: - self.val_int = int(val_int) - if val_str: - self.val_str = str(val_str) - if val_radio: - self.val_radio = val_radio - if val_check is not None: - print(val_check) - self.val_check = val_check if (type(val_check) is list) else [val_check] - if val_select: - self.val_select = val_select - - if val_disposable: - print("DISPOSABLE VALUE: {}".format(val_disposable)) - - def get_values_dict(self): - return { - 'val_int': self.val_int, - 'val_str': self.val_str, - 'val_radio': self.val_radio, - 'val_check': self.val_check, - 'val_select': self.val_select, - 'val_unused': self.val_unused - } - - def generate_random_numbers_for_a_while(self, run_time: int): - self.run_time = run_time - vals = [] - for _ in range(run_time): - vals.append(random.random()) - time.sleep(1) - - return vals \ No newline at end of file diff --git a/openflexure_microscope/plugins/testing/schema_example/schema.json b/openflexure_microscope/plugins/testing/schema_example/schema.json deleted file mode 100644 index 4aee4dcd..00000000 --- a/openflexure_microscope/plugins/testing/schema_example/schema.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "id": "test-plugin", - "icon": "pets", - "forms": [ - { - "name": "Simple request", - "isCollapsible": false, - "isTask": false, - "selfUpdate": true, - "route": "/do", - "submitLabel": "Do things", - "schema": [ - [ - { - "fieldType": "numberInput", - "placeholder": "Some integer", - "name": "val_int", - "label": "Number value", - "minValue": 0 - }, - { - "fieldType": "textInput", - "placeholder": "Some string", - "label": "String value", - "name": "val_str" - } - ], - [ - { - "fieldType": "radioList", - "name": "val_radio", - "label": "Radio value", - "options": ["First", "Second", "Third"] - }, - { - "fieldType": "checkList", - "name": "val_check", - "label": "Checklist values", - "options": ["Foo", "Bar", "Baz"] - } - ], - { - "fieldType": "htmlBlock", - "name": "html_block", - "content": "This is a block of HTML in a plugin!
I can do paragraph breaks and stuff." - }, - { - "fieldType": "selectList", - "name": "val_select", - "multi": false, - "label": "Some selection", - "options": ["Most", "Average", "Least"] - }, - - { - "fieldType": "textInput", - "placeholder": "Some string", - "label": "Non-persistent string", - "name": "val_disposable" - } - ] - }, - { - "name": "Task form", - "isTask": true, - "selfUpdate": true, - "route": "/task", - "submitLabel": "Start task", - "schema": [ - { - "fieldType": "numberInput", - "placeholder": "", - "name": "run_time", - "label": "Run time (seconds)", - "minValue": 1 - } - ] - } - ] -} \ No newline at end of file diff --git a/openflexure_microscope/plugins/testing/__init__.py b/openflexure_microscope/rescue/__init__.py similarity index 100% rename from openflexure_microscope/plugins/testing/__init__.py rename to openflexure_microscope/rescue/__init__.py diff --git a/openflexure_microscope/rescue/auto.py b/openflexure_microscope/rescue/auto.py new file mode 100644 index 00000000..9983c1a9 --- /dev/null +++ b/openflexure_microscope/rescue/auto.py @@ -0,0 +1,77 @@ +from openflexure_microscope.paths import ( + FALLBACK_OPENFLEXURE_VAR_PATH, + PREFERRED_OPENFLEXURE_VAR_PATH, +) +import logging +import os + +from . import check_settings, check_capture_reload + +# Paths for suggestions +LOGS_PATHS = [ + os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "logs"), + os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "logs"), +] +SETTINGS_PATHS = [ + os.path.join(var_path, "settings", "microscope_settings.json") + for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH) +] +CONFIG_PATHS = [ + os.path.join(var_path, "settings", "microscope_configuration.json") + for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH) +] +DATA_PATHS = [ + os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "data"), + os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "data"), +] + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +logging.debug("Testing debug logger. One two one two.") + + +class bcolors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +error_keys = { + "config_settings_import_error": "The configuration submodule could not be imported properly. This is usually because the default config or settings files are badly broken somehow. See further errors for details", + "default_config_empty": "The default configuration file is empty, and could not be automatically populated.", + "default_settings_empty": "The default settings file is empty, and could not be automatically populated.", + "default_config_error": f"The default configuration file could not be parsed. To fix, consider backing up and deleting your configuration files from {CONFIG_PATHS} to reset the configuration.", + "default_settings_error": f"The default settings file could not be parsed. To fix, consider backing up and deleting your configuration files from {SETTINGS_PATHS} to reset the configuration.", + "capture_rebuild_timeout": f"Capture database rebuilding took a long time. This may not cause catastrophic errors, but rather will cause the server to hang for a while. To fix, consider moving your captures from {DATA_PATHS} to another location.", +} + +if __name__ == "__main__": + spoof = False + + error_sources = [] + + if spoof: + error_sources = list(error_keys.keys()) + + error_sources.extend(check_settings.main()) + error_sources.extend(check_capture_reload.main()) + + if not error_sources: + + print() + print(bcolors.OKGREEN + "No errors found!" + bcolors.ENDC) + print( + "That's not to say everything is fine, only that our automatic diagnostics couldn't find much." + ) + print(f"You can check through the server logs at {LOGS_PATHS}") + + else: + for err_code in error_sources: + logging.error(err_code) + if error_keys.get(err_code): + print(bcolors.FAIL + error_keys.get(err_code) + bcolors.ENDC) diff --git a/openflexure_microscope/rescue/check_capture_reload.py b/openflexure_microscope/rescue/check_capture_reload.py new file mode 100644 index 00000000..c79d76d6 --- /dev/null +++ b/openflexure_microscope/rescue/check_capture_reload.py @@ -0,0 +1,34 @@ +from openflexure_microscope.rescue.monitor_timeout import launch_timeout_test_process +from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH +from openflexure_microscope.captures.capture import build_captures_from_exif +from openflexure_microscope.config import user_settings + +import logging + + +def check_capture_rebuild(timeout=10): + logging.info("Loading user settings...") + settings = user_settings.load() + + cap_path = str(settings.get("captures", {}).get("paths", {}).get("default")) + logging.info(f"Capture path found: {cap_path}") + if not cap_path: + logging.error( + "No capture path defined in settings. This is unusual for anything other than a first-run. \nFalling back to default path." + ) + cap_path = BASE_CAPTURE_PATH + + logging.info("Starting capture reload with a 10 second timeout...") + passed_timeout_test = launch_timeout_test_process( + build_captures_from_exif, args=(cap_path,) + ) + + return passed_timeout_test + + +def main(): + error_sources = [] + passed_timeout = check_capture_rebuild() + if not passed_timeout: + error_sources.append("capture_rebuild_timeout") + return error_sources diff --git a/openflexure_microscope/rescue/check_settings.py b/openflexure_microscope/rescue/check_settings.py new file mode 100644 index 00000000..9ba33ba7 --- /dev/null +++ b/openflexure_microscope/rescue/check_settings.py @@ -0,0 +1,47 @@ +import logging +import json + +ERROR_SOURCES = [] + + +def trace_config_exceptions(): + error_sources = [] + + from openflexure_microscope.paths import ( + DEFAULT_CONFIGURATION_FILE_PATH, + SETTINGS_FILE_PATH, + ) + + try: + default_config = json.load(DEFAULT_CONFIGURATION_FILE_PATH) + if not default_config: + error_sources.append("default_config_empty") + except Exception as e: + logging.error("Error parsing config:") + logging.error(e) + error_sources.append("default_config_error") + + try: + default_settings = json.load(SETTINGS_FILE_PATH) + if not default_settings: + error_sources.append("default_settings_empty") + except Exception as e: + logging.error("Error parsing settings:") + logging.error(e) + error_sources.append("default_settings_error") + + return error_sources + + +def main(): + error_sources = [] + logging.info("Attempting default settings and config import...") + try: + from openflexure_microscope import config + except Exception as e: + error_sources.append("config_settings_import_error") + logging.error("Error importing config:") + logging.error(e) + error_sources.extend(trace_config_exceptions()) + + return error_sources diff --git a/openflexure_microscope/rescue/monitor_service.py b/openflexure_microscope/rescue/monitor_service.py new file mode 100644 index 00000000..f12ab1b9 --- /dev/null +++ b/openflexure_microscope/rescue/monitor_service.py @@ -0,0 +1,59 @@ +#!/bin/python +# +# Copyright 2016 Flavio Garcia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Usage: monitor_process.py +# +# Example(crontab, every 5 minutes): +# */5 * * * * /root/bin/monitor_service.py prosody > /dev/null 2>&1 +# + +import sys +import subprocess + + +class ServiceMonitor(object): + def __init__(self, service): + self.service = service + + def is_active(self): + """Return True if service is running""" + for line in self.status(): + if "Active:" in line: + if "(running)" in line: + return True + return False + + def status(self): + cmd = f"/bin/systemctl status {self.service}.service" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + stdout_list = proc.communicate()[0].decode().split("\n") + return stdout_list + + def start(self): + cmd = f"/bin/systemctl start {self.service}.service" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + proc.communicate() + + def stop(self): + cmd = f"/bin/systemctl stop {self.service}.service" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + proc.communicate() + + def log(self, n_lines: int = 100): + cmd = f"/bin/journalctl -u {self.service}.service -n {n_lines} --no-pager" + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + stdout_list = proc.communicate()[0].decode().split("\n") + return stdout_list diff --git a/openflexure_microscope/rescue/monitor_timeout.py b/openflexure_microscope/rescue/monitor_timeout.py new file mode 100644 index 00000000..b02b29b6 --- /dev/null +++ b/openflexure_microscope/rescue/monitor_timeout.py @@ -0,0 +1,39 @@ +import multiprocessing +import logging +import time + +# bar +def test_long_fn(): + for _ in range(100): + print("Tick") + time.sleep(1) + + +def launch_timeout_test_process(target, args=(), kwargs=None, timeout=10): + if not kwargs: + kwargs = {} + + # Start target as a process + p = multiprocessing.Process(target=target, args=args, kwargs=kwargs) + p.start() + + # Wait for 10 seconds or until process finishes + p.join(timeout) + + # If thread is still active + if p.is_alive(): + logging.error( + f"Function {target} reached timeout after {timeout} seconds. Terminating." + ) + + # Terminate + p.terminate() + p.join() + + return False + else: + return True + + +if __name__ == "__main__": + launch_timeout_test_process(test_long_fn, timeout=5) diff --git a/openflexure_microscope/stage/__init__.py b/openflexure_microscope/stage/__init__.py index 46cd36a8..b159fe5f 100644 --- a/openflexure_microscope/stage/__init__.py +++ b/openflexure_microscope/stage/__init__.py @@ -1,3 +1,3 @@ -__all__ = ['base', 'mock', 'sanga'] +__all__ = ["base", "mock", "sanga"] from . import base, mock, sanga diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index 13d23be9..50c3fc2c 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -1,34 +1,38 @@ +import numpy as np from abc import ABCMeta, abstractmethod -from openflexure_microscope.lock import StrictLock +from labthings import StrictLock class BaseStage(metaclass=ABCMeta): """ Attributes: - lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread + lock (:py:class:`labthings.StrictLock`): Strict lock controlling thread access to camera hardware """ + def __init__(self): - self.lock = StrictLock(timeout=5) + self.lock = StrictLock(name="Stage", timeout=None) @abstractmethod - def apply_config(self, config: dict): + def update_settings(self, config: dict): """Update settings from a config dictionary""" pass @abstractmethod - def read_config(self): + def read_settings(self): """Return the current settings as a dictionary""" pass @property @abstractmethod def state(self): - """The general state dictionary of the board. - Should at least contain 'position', and 'board' keys. - Note: A None/Null value for 'board' will disable stage - movement in the OpenFlexure eV client software, - """ + """The general state dictionary of the board.""" + pass + + @property + @abstractmethod + def configuration(self): + """The general stage configuration.""" pass @property @@ -43,6 +47,10 @@ class BaseStage(metaclass=ABCMeta): """The current position, as a list""" pass + @property + def position_map(self): + return {"x": self.position[0], "y": self.position[1], "z": self.position[2]} + @property @abstractmethod def backlash(self): @@ -68,7 +76,50 @@ class BaseStage(metaclass=ABCMeta): """Make an absolute move to a position""" pass + @abstractmethod + def zero_position(self): + """Set the current position to zero""" + pass + @abstractmethod def close(self): """Cleanly close communication with the stage""" pass + + def scan_linear(self, rel_positions, backlash=True, return_to_start=True): + """ + Scan through a list of (relative) positions (generator fn) + rel_positions should be an nx3-element array (or list of 3 element arrays). + Positions should be relative to the starting position - not a list of relative moves. + backlash argument is passed to move_rel + if return_to_start is True (default) we return to the starting position after a + successful scan. NB we always attempt to return to the starting position if an + exception occurs during the scan.. + """ + starting_position = self.position + rel_positions = np.array(rel_positions) + assert rel_positions.shape[1] == 3, ValueError( + "Positions should be 3 elements long." + ) + try: + self.move_rel(rel_positions[0], backlash=backlash) + yield 0 + + for i, step in enumerate(np.diff(rel_positions, axis=0)): + self.move_rel(step, backlash=backlash) + yield i + 1 + except Exception as e: + return_to_start = True # always return to start if it went wrong. + raise e + finally: + if return_to_start: + self.move_abs(starting_position, backlash=backlash) + + def scan_z(self, dz, **kwargs): + """Scan through a list of (relative) z positions (generator fn) + This function takes a 1D numpy array of Z positions, relative to + the position at the start of the scan, and converts it into an + array of 3D positions with x=y=0. This, along with all the + keyword arguments, is then passed to ``scan_linear``. + """ + return self.scan_linear([[0, 0, z] for z in dz], **kwargs) diff --git a/openflexure_microscope/stage/mock.py b/openflexure_microscope/stage/mock.py index 0c2ddfec..fe523c64 100644 --- a/openflexure_microscope/stage/mock.py +++ b/openflexure_microscope/stage/mock.py @@ -3,47 +3,41 @@ from openflexure_microscope.utilities import axes_to_array from collections.abc import Iterable import numpy as np +import time +import logging -class MockStage(BaseStage): + +class MissingStage(BaseStage): def __init__(self, port=None, **kwargs): BaseStage.__init__(self) self._position = [0, 0, 0] self._n_axis = 3 self._backlash = None + self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis + @property def state(self): """The general state dictionary of the board.""" - state = { - 'position': { - 'x': self.position[0], - 'y': self.position[1], - 'z': self.position[2], - }, - 'board': None, - 'version': '0' - } + state = {"position": self.position_map} return state - def apply_config(self, config: dict): - """Update settings from a config dictionary""" + @property + def configuration(self): + return {} + def update_settings(self, config: dict): + """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels - if 'backlash' in config: + if "backlash" in config: # Construct backlash array - backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0]) + backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0]) self.backlash = backlash - def read_config(self) -> dict: + def read_settings(self) -> dict: """Return the current settings as a dictionary""" blsh = self.backlash.tolist() - config = { - 'backlash': { - 'x': blsh[0], - 'y': blsh[1], - 'z': blsh[2], - } - } + config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}} return config @property @@ -69,13 +63,37 @@ class MockStage(BaseStage): assert len(blsh) == self.n_axes self._backlash = np.array(blsh) else: - self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int) + self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) - def move_rel(self, displacement, axis=None, backlash=True): - pass + def move_rel( + self, displacement: list, axis=None, backlash=True, simulate_time: bool = True + ): + if simulate_time: + time.sleep(0.5) + if axis is not None: + assert axis in self.axis_names, "axis must be one of {}".format( + self.axis_names + ) + move = np.zeros(self.n_axes, dtype=np.int) + move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement) + displacement = move - def move_abs(self, final, **kwargs): - pass + initial_move = np.array(displacement, dtype=np.int) + + self._position = list(np.array(self._position) + np.array(initial_move)) + logging.debug(np.array(self._position) + np.array(initial_move)) + logging.debug(f"New position: {self._position}") + + def move_abs(self, final, simulate_time: bool = True, **kwargs): + if simulate_time: + time.sleep(0.5) + + self._position = list(final) + logging.debug(f"New position: {self._position}") + + def zero_position(self): + """Set the current position to zero""" + self._position = [0, 0, 0] def close(self): pass diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index 01de2651..9cd8cf6b 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -2,7 +2,7 @@ import numpy as np import time import logging from collections.abc import Iterable -from .sangaboard import Sangaboard +from sangaboard import Sangaboard from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.utilities import axes_to_array @@ -18,28 +18,31 @@ class SangaStage(BaseStage): board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object. _backlash (list): 3-element (element-per-axis) list of backlash compensation in steps. """ + def __init__(self, port=None, **kwargs): """Class managing serial communications with the motors for an Openflexure stage""" BaseStage.__init__(self) + self.port = port self.board = Sangaboard(port, **kwargs) - self._backlash = None # Initialise backlash storage, used by property setter/getter - self.axis_names = ['x', 'y', 'z'] # Assume all sangaboards are 3 axis + self._backlash = ( + None + ) # Initialise backlash storage, used by property setter/getter + self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis @property def state(self): """The general state dictionary of the board.""" - state = { - 'position': { - 'x': self.position[0], - 'y': self.position[1], - 'z': self.position[2], - }, - 'board': self.board.board, - 'firmware': self.board.firmware + return {"position": self.position_map} + + @property + def configuration(self): + return { + "port": self.port, + "board": self.board.board, + "firmware": self.board.firmware, } - return state @property def n_axes(self): @@ -48,7 +51,8 @@ class SangaStage(BaseStage): @property def position(self): - return self.board.position + with self.lock(timeout=None): + return self.board.position @property def backlash(self): @@ -80,37 +84,25 @@ class SangaStage(BaseStage): assert len(blsh) == self.n_axes self._backlash = np.array(blsh) else: - self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int) + self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) - def apply_config(self, config: dict): + def update_settings(self, config: dict): """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels - if 'backlash' in config: + if "backlash" in config: # Construct backlash array - backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0]) + backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0]) self.backlash = backlash - def read_config(self) -> dict: + def read_settings(self) -> dict: """Return the current settings as a dictionary""" blsh = self.backlash.tolist() - config = { - 'backlash': { - 'x': blsh[0], - 'y': blsh[1], - 'z': blsh[2], - } - } + config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}} return config - def steps_to_array(self, displacement, axis): - assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names) - move = np.zeros(self.n_axes, dtype=np.int) - move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement) - return move - - def move_rel(self, displacement, axis=None, backlash=True): + def move_rel(self, displacement: list, axis=None, backlash=True): """Make a relative move, optionally correcting for backlash. displacement: integer or array/list of 3 integers axis: None (for 3-axis moves) or one of 'x','y','z' @@ -121,8 +113,18 @@ class SangaStage(BaseStage): displacement = self.steps_to_array(displacement, axis) with self.lock: + logging.debug(f"Moving sangaboard by {displacement}") if not backlash or self.backlash is None: return self.board.move_rel(displacement, axis=axis) + if axis is not None: + # backlash correction is easier if we're always in 3D + # so this code just converts single-axis moves into all-axis moves. + assert axis in self.axis_names, "axis must be one of {}".format( + self.axis_names + ) + move = np.zeros(self.n_axes, dtype=np.int) + move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement) + displacement = move initial_move = np.array(displacement, dtype=np.int) # Backlash Correction @@ -136,7 +138,7 @@ class SangaStage(BaseStage): initial_move -= np.where( self.backlash * displacement < 0, self.backlash, - np.zeros(self.n_axes, dtype=self.backlash.dtype) + np.zeros(self.n_axes, dtype=self.backlash.dtype), ) self.board.move_rel(initial_move) if np.any(displacement - initial_move != 0): @@ -148,47 +150,17 @@ class SangaStage(BaseStage): """Make an absolute move to a position """ with self.lock: + logging.debug(f"Moving sangaboard to {final}") self.board.move_abs(final, **kwargs) - def scan_linear(self, rel_positions, backlash=True, return_to_start=True): - """ - Scan through a list of (relative) positions (generator fn) - rel_positions should be an nx3-element array (or list of 3 element arrays). - Positions should be relative to the starting position - not a list of relative moves. - backlash argument is passed to move_rel - if return_to_start is True (default) we return to the starting position after a - successful scan. NB we always attempt to return to the starting position if an - exception occurs during the scan.. - """ - starting_position = self.position - rel_positions = np.array(rel_positions) - assert rel_positions.shape[1] == 3, ValueError("Positions should be 3 elements long.") - try: - self.move_rel(rel_positions[0], backlash=backlash) - yield 0 - - for i, step in enumerate(np.diff(rel_positions, axis=0)): - self.move_rel(step, backlash=backlash) - yield i + 1 - except Exception as e: - return_to_start = True # always return to start if it went wrong. - raise e - finally: - if return_to_start: - self.move_abs(starting_position, backlash=backlash) - - def scan_z(self, dz, **kwargs): - """Scan through a list of (relative) z positions (generator fn) - This function takes a 1D numpy array of Z positions, relative to - the position at the start of the scan, and converts it into an - array of 3D positions with x=y=0. This, along with all the - keyword arguments, is then passed to ``scan_linear``. - """ - return self.scan_linear([[0, 0, z] for z in dz], **kwargs) + def zero_position(self): + """Set the current position to zero""" + with self.lock: + self.board.zero_position() def close(self): """Cleanly close communication with the stage""" - if hasattr(self, 'board'): + if hasattr(self, "board"): self.board.close() # Methods specific to Sangaboard @@ -207,13 +179,17 @@ class SangaStage(BaseStage): need to worry about that here. """ if type is not None: - print("An exception occurred inside a with block, resetting position \ - to its value at the start of the with block") + print( + "An exception occurred inside a with block, resetting position \ + to its value at the start of the with block" + ) try: time.sleep(0.5) self.move_abs(self._position_on_enter) except Exception as e: - print("A further exception occurred when resetting position: {}".format(e)) + print( + "A further exception occurred when resetting position: {}".format(e) + ) print("Move completed, raising exception...") raise value # Propagate the exception diff --git a/openflexure_microscope/stage/sangaboard/__init__.py b/openflexure_microscope/stage/sangaboard/__init__.py deleted file mode 100644 index 1904bd7f..00000000 --- a/openflexure_microscope/stage/sangaboard/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -### THIS WILL EVENTUALLY BE REPLACED WITH A DEPENDENCY ON PYSANGABOARD ### -from .sangaboard import Sangaboard - -__version__ = "0.1" diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py deleted file mode 100644 index c4fc45ce..00000000 --- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py +++ /dev/null @@ -1,422 +0,0 @@ -# -*- coding: utf-8 -*- -""" -This module has been modified from the chopped-out of class from nplab_. -It is a serial instrument class to simplify the process of interfacing with -equipment that talks on a serial port. The idea is that your instrument can -subclass :class:`ExtensibleSerialInstrument` and provide methods to control the -hardware, which will mostly consist of `self.query()` commands. It also has -options for adding OptionalModules so that you don't have to have multiple -classes for lots of instruments with different configurations - -The :class:`QueriedProperty` class is a convenient shorthand to create a property -that is read and/or set with a single serial query (i.e. a read followed by a write). - -.. module_author: Richard Rowman (c) 2017, released under GNU GPL -.. _nplab: http://www.github.com/nanophotonics/nplab -""" - - -from __future__ import division -import re -from functools import partial -import threading -import serial -from serial import FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS -from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE -from serial import STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO -import io -import time -import logging -import warnings - -class ExtensibleSerialInstrument(object): - """ - An instrument that communicates by sending strings back and forth over serial - - This base class provides commonly-used mechanisms that support the use of - serial instruments. Most interactions with this class involve - a call to the `query` method. This writes a message and returns the reply. - This has been hacked together from the nplab_ MessageBusInstrument and SerialInstrument - classes. - - **Threading Notes** - - The message bus protocol includes a property, `communications_lock`. All - commands that use the communications bus should be protected by this lock. - It's also permissible to use it to protect sequences of calls to the bus - that must be atomic (e.g. a multi-part exchange of messages). However, try - not to hold it too long - or odd things might happen if other threads are - blocked for a long time. The lock is reentrant so there's no issue with - acquiring it twice. - """ - termination_character = "\n" #: All messages to or from the instrument end with this character. - termination_line = None #: If multi-line responses are recieved, they must end with this string - ignore_echo = False - port_settings = {} - - def __init__(self, port=None, **kwargs): - """ - Set up the serial port and so on. - """ - logging.info("Updating ESI port settings") - self.port_settings.update(kwargs) - logging.info("Opening ESI connection to port {}".format(port)) - self.open(port, False) # Eventually this shouldn't rely on init... - logging.info("Opened ESI connection to port {}".format(port)) - - def open(self, port=None, quiet=True): - """Open communications with the serial port. - - If no port is specified, it will attempt to autodetect. If quiet=True - then we don't warn when ports are opened multiple times. - """ - with self.communications_lock: - if hasattr(self,'_ser') and self._ser.isOpen(): - if not quiet: logging.warning("Attempted to open an already-open port!") - return - if port is None: - port=self.find_port() - assert port is not None, "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?" - self._ser = serial.Serial(port,**self.port_settings) - #the block above wraps the serial IO layer with a text IO layer - #this allows us to read/write in neat lines. NB the buffer size must - #be set to 1 byte for maximum responsiveness. - assert self.test_communications(), "The instrument doesn't seem to be responding. Did you specify the right port?" - - def close(self): - """Cleanly close the device. Includes proper logging statements.""" - logging.debug("Closing serial connection") - with self.communications_lock: - try: - self._ser.close() - except Exception as e: - logging.warning("The serial port didn't close cleanly: {}".format(e)) - logging.debug("Connection closed") - - def __del__(self): - """Emergency close the device. Try to avoid having to use this.""" - if hasattr(self, '_ser') and self._ser.isOpen(): - with self.communications_lock: - print( - "Closing an open serial communication has been triggered by garbage collection!\n"\ - "Please close this device more sensibly in future.") - try: - self._ser.close() - except Exception as e: - print("The serial port didn't close cleanly: {}".format(e)) - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - """Cleanly close down the instrument at end of a with block.""" - self.close() - - def write(self,query_string): - """Write a string to the serial port""" - with self.communications_lock: - assert self._ser.isOpen(), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?" - #TODO: Check if this code is needed and if not kill it -# try: -# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting -# except AttributeError: -# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting - data=query_string+self.termination_character - data=data.encode() - self._ser.write(data) - - def flush_input_buffer(self): - """Make sure there's nothing waiting to be read, and clear the buffer if there is.""" - with self.communications_lock: - if self._ser.inWaiting()>0: self._ser.flushInput() - def readline(self, timeout=None): - """Read one line from the serial port.""" - with self.communications_lock: - return self._ser.readline().decode('utf8').replace(self.termination_character,"\n") - - _communications_lock = None - @property - def communications_lock(self): - """A lock object used to protect access to the communications bus""" - # This requires initialisation but our init method won't be called - so - # the property initialises it on first use. - if self._communications_lock is None: - self._communications_lock = threading.RLock() - return self._communications_lock - - def read_multiline(self, termination_line=None, timeout=None): - """Read one line from the underlying bus. Must be overriden. - - This should not need to be reimplemented unless there's a more efficient - way of reading multiple lines than multiple calls to readline().""" - with self.communications_lock: - if termination_line is None: - termination_line = self.termination_line - assert isinstance(termination_line, str), "If you perform a multiline query, you must specify a termination line either through the termination_line keyword argument or the termination_line property of the NPSerialInstrument." - response = "" - last_line = "dummy" - while termination_line not in last_line and len(last_line) > 0: #read until we get the termination line. - last_line = self.readline(timeout) - response += last_line - return response - - def query(self,queryString,multiline=False,termination_line=None,timeout=None): - """ - Write a string to the stage controller and return its response. - - It will block until a response is received. The multiline and termination_line commands - will keep reading until a termination phrase is reached. - """ - with self.communications_lock: - self.flush_input_buffer() - self.write(queryString) - if self.ignore_echo == True: # Needs Implementing for a multiline read! - first_line = self.readline(timeout).strip() - if first_line == queryString: - return self.readline(timeout).strip() - else: - logging.info('This command did not echo!!!') - return first_line - - if termination_line is not None: - multiline = True - if multiline: - return self.read_multiline(termination_line) - else: - return self.readline(timeout).strip() #question: should we strip the final newline? - - def parsed_query(self, query_string, response_string=r"%d", re_flags=0, parse_function=None, **kwargs): - """ - Perform a query, returning a parsed form of the response. - - First query the instrument with the given query string, then compare - the response against a template. The template may contain text and - placeholders (e.g. %i and %f for integer and floating point values - respectively). Regular expressions are also allowed - each group is - considered as one item to be parsed. However, currently it's not - supported to use both % placeholders and regular expressions at the - same time. - - If placeholders %i, %f, etc. are used, the returned values are - automatically converted to integer or floating point, otherwise you - must specify a parsing function (applied to all groups) or a list of - parsing functions (applied to each group in turn). - """ - - response_regex = response_string - noop = lambda x: x #placeholder null parse function - placeholders = [ #tuples of (regex matching placeholder, regex to replace it with, parse function) - (r"%c", r".", noop), - (r"%(\d+)c", r".{\1}", noop), #TODO support %cn where n is a number of chars - (r"%d", r"[-+]?\\d+", int), - (r"%[eEfg]", r"[-+]?(?:\\d+(?:\.\\d*)?|\.\\d+)(?:[eE][-+]?\\d+)?", float), - (r"%i", r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)", lambda x: int(x, 0)), #0=autodetect base - (r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), #8 means octal - (r"%s", r"\\s+", noop), - (r"%u", r"\\d+", int), - (r"%[xX]", r"[-+]?(?:0[xX])?[\\dA-Fa-f]+", lambda x: int(x, 16)), #16 forces hexadecimal - ] - matched_placeholders = [] - for placeholder, regex, parse_fun in placeholders: - response_regex = re.sub(placeholder, '('+regex+')', response_regex) #substitute regex for placeholder - matched_placeholders.extend([(parse_fun, m.start()) for m in re.finditer(placeholder, response_string)]) #save the positions of the placeholders - if parse_function is None: - parse_function = [f for f, s in sorted(matched_placeholders, key=lambda m: m[1])] #order parse functions by their occurrence in the original string - if not hasattr(parse_function,'__iter__'): - parse_function = [parse_function] #make sure it's a list. - - reply = self.query(query_string, **kwargs) #do the query - #if match this could be because another response entered the buffer between write and read. Sleep for short while then - #check if something is now in the buffer, while the buffer is not empty repeat regex - waited=False - res=re.search(response_regex, reply, flags=re_flags) - while res is None: - if not waited: - time.sleep(.1) - waited=True - original_reply = reply - if self._ser.inWaiting(): - reply = self.readline().strip() - res=re.search(response_regex, reply, flags=re_flags) - if res is not None: - warnings.warn("Query suceeded after initially receieving unmatched response ('%s') to '%s'. Match pattern /%s/ (generated regex /%s/)"%(original_reply, query_string, response_string, response_regex), RuntimeWarning) - else: - raise ValueError("Stage response to '%s' ('%s') wasn't matched by /%s/ (generated regex /%s/)" % (query_string, original_reply, response_string, response_regex)) - try: - parsed_result= [f(g) for f, g in zip(parse_function, res.groups())] #try to apply each parse function to its argument - if len(parsed_result) == 1: - return parsed_result[0] - else: - return parsed_result - except ValueError: - logging.info("Matched Groups: {}".format(res.groups())) - logging.info("Parsing Functions {}:".format(parse_function)) - raise ValueError("Stage response to %s ('%s') couldn't be parsed by the supplied function" % (query_string, reply)) - def int_query(self, query_string, **kwargs): - """Perform a query and return the result(s) as integer(s) (see parsedQuery)""" - return self.parsed_query(query_string, "%d", **kwargs) - def float_query(self, query_string, **kwargs): - """Perform a query and return the result(s) as float(s) (see parsedQuery)""" - return self.parsed_query(query_string, "%f", **kwargs) - - def test_communications(self): - """Check if the device is available on the current port. - - This should be overridden by subclasses. Assume the port has been - successfully opened and the settings are as defined by self.port_settings. - Usually this function sends a command and checks for a known reply.""" - with self.communications_lock: - return True - - def find_port(self): - """Iterate through the available serial ports and query them to see - if our instrument is there.""" - with self.communications_lock: - success = False - for port_name, _, _ in serial.tools.list_ports.comports(): #loop through serial ports, apparently 256 is the limit?! - try: - logging.info("Trying port {}".format(port_name)) - self.open(port_name) - success = True - logging.info("Success!") - except: - pass - finally: - try: - self.close() - except: - pass #we don't care if there's an error closing the port... - if success: - break #again, make sure this happens *after* closing the port - if success: - return port_name - else: - return None - -class OptionalModule(object): - """This allows a `ExtensibleSerialInstrument` to have optional features. - - OptionalModule is designed as a base class - for interfacing with optional modules which may or may not be included with - the serial instrument, and can be added or removed at run-time. - """ - - def __init__(self,available,parent=None,module_type="Undefined",model="Generic"): - assert type(available) is bool, 'Option module availablity should be a boolean not a {}'.format(type(available)) - self._available=available - self._parent=parent - assert type(module_type) is str, 'Option module type should be a string not a {}'.format(type(module_typ)) - self.module_type=module_type - if available: - assert type(model) is str, 'Option module type should be a string not a {}'.format(type(model)) - self.model=model - else: - self.model=None - - @property - def available(self): - return self._available - - def confirm_available(self): - """Check if module is available, no return, will raise exception if not available!""" - assert self._available, "No \"{}\" supported on firmware".format(self.module_type) - - def describe(self): - """Consistently spaced desciption for listing modules""" - return self.module_type+" "*(25-len(self.module_type))+"- "+self.model - - -class QueriedProperty(object): - """A Property interface that reads and writes from the instrument on the bus. - - This returns a property-like (i.e. a descriptor) object. You can use it - in a class definition just like a property. The property it creates will - interact with the instrument over the communication bus to set and retrieve - its value. It uses calls to `ExtensibleSerialInstrument.parsed_query` to set or - get the value of the property. - - `QueriedProperty` can be used to define properties on a `ExtensibleSerialInstrument` - or an `OptionalModule` (in which case the `ExtensibleSerialInstrument.parsed_query` - method of the parent object will be used). - - Arguments: - - :get_cmd: - the string sent to the instrument to obtain the value - :set_cmd: - the string used to set the value (use {} or % placeholders) - :validate: - a list of allowable values - :valrange: - a maximum and minimum value - :fdel: - a function to call when it's deleted - :doc: - the docstring - :response_string: - supply a % code (as you would for response_string in a - ``ExtensibleSerialInstrument.parsed_query``) - :ack_writes: - set to "readline" to discard a line of input after writing. - """ - def __init__(self, get_cmd=None, set_cmd=None, validate=None, valrange=None, - fdel=None, doc=None, response_string=None, ack_writes="no"): - self.response_string = response_string - self.get_cmd = get_cmd - self.set_cmd = set_cmd - self.validate = validate - self.valrange = valrange - self.fdel = fdel - self.ack_writes = ack_writes - self.__doc__ = doc - - - # TODO: standardise the return (single value only vs parsed result), consider bool - def __get__(self, obj, objtype=None): - if issubclass(type(obj),OptionalModule): - obj.confirm_available() - obj=obj._parent - if obj is None: - return self - assert issubclass(type(obj),ExtensibleSerialInstrument) - if self.get_cmd is None: - raise AttributeError("unreadable attribute") - # Allow certain "magic" values to set the response string - for key, val in [('float',r"%f"), - ('int',r"%d"),]: - if self.response_string == key: - self.response_string = val - if self.response_string in ['bool', 'raw', None]: - value = obj.query(self.get_cmd) - if self.response_string == 'bool': - value = bool(value) - else: - value = obj.parsed_query(self.get_cmd, self.response_string) - return value - - def __set__(self, obj, value): - if issubclass(type(obj),OptionalModule): - obj.confirm_available() - obj=obj._parent - assert issubclass(type(obj),ExtensibleSerialInstrument) - if self.set_cmd is None: - raise AttributeError("can't set attribute") - if self.validate is not None: - if value not in self.validate: - raise ValueError('invalid value supplied - value must be one of {}'.format(self.validate)) - if self.valrange is not None: - if value < min(self.valrange) or value > max(self.valrange): - raise ValueError('invalid value supplied - value must be in the range {}-{}'.format(*self.valrange)) - message = self.set_cmd - if '{0' in message: - message = message.format(value) - elif '%' in message: - message = message % value - obj.write(message) - if self.ack_writes == "readline": - obj.readline() - - def __delete__(self, obj): - if self.fdel is None: - raise AttributeError("can't delete attribute") - self.fdel(obj) diff --git a/openflexure_microscope/stage/sangaboard/sangaboard.py b/openflexure_microscope/stage/sangaboard/sangaboard.py deleted file mode 100644 index 33c8f3bd..00000000 --- a/openflexure_microscope/stage/sangaboard/sangaboard.py +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -""" -Sangaboard module - -This Python code allows control of the Sangaboard - -It is (c) Richard Bowman & Julian Stirling 2019 and released under GNU GPL v3 -""" -from __future__ import print_function, division -import time -from .extensible_serial_instrument import ExtensibleSerialInstrument, OptionalModule, QueriedProperty, EIGHTBITS, PARITY_NONE, STOPBITS_ONE -import serial, serial.tools.list_ports -import re -import warnings -import logging - -class Sangaboard(ExtensibleSerialInstrument): - """Class managing serial communications with a Sangaboard - - The `Sangaboard` class handles setting up communications with the sangaboard, - wraps the various serial commands in Python methods, and provides iterators and - context managers to simplify opening/closing the hardware connection and some - other tasks like conducting a linear scan. - - Arguments to the constructor are passed to the constructor of - :class:`Sangaboard.extensible_serial_instrument.ExtensibleSerialInstrument`, - most likely the only one necessary is `port` which should be set to the serial port - you will use to communicate with the motor controller. - - This class can be used as a context manager, i.e. it's encouraged to use it as:: - - with Sangaboard() as sb: - sb.move_rel([1000,0,0]) - - In that case, the serial port will automatically be closed at the end of the block, - even if an error occurs. Otherwise, be sure to call the - :meth:`~.ExtensibleSerialInstrument.close()` method to release the serial port. - """ - - # List of valid and deprecated firmwares, for communication checks - valid_firmwares = [ - (0, 5), - (0, 4) - ] - deprecated_firmwares = [ - (0, 4) - ] - - # These are the settings for the sangaboards serial port, and can usually be left as default. - port_settings = {'baudrate':115200, 'bytesize':EIGHTBITS, 'parity':PARITY_NONE, 'stopbits':STOPBITS_ONE} - - # position, step time and ramp time are get/set using simple serial commands. - position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d", - doc="Get the position of the axes as a tuple of 3 integers.") - step_time = QueriedProperty(get_cmd="dt?", set_cmd="dt %d", response_string="minimum step delay %d", - doc="Get or set the minimum time between steps of the motors in microseconds.\n\n" - "The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on " - "the sangaboard, so it will be persistent even if the motor controller is turned off.") - ramp_time = QueriedProperty(get_cmd="ramp_time?", set_cmd="ramp_time %d", response_string="ramp time %d", - doc="Get or set the acceleration time in microseconds.\n\n" - "The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` " - "microseconds. Zero means the motor runs at full speed initially, with no accleration " - "control. Small moves may last less than `2*ramp_time`, in which case the acceleration " - "will be the same, but the motor will never reach full speed. It is saved to EEPROM on " - "the sangaboard, so it will be persistent even if the motor controller is turned off.") - - # The names of the sangaboard's axes. NB this also defines the number of axes - axis_names = ('x', 'y', 'z') - - # Once initialised, `firmware` is a string that identifies the firmware version - firmware = None - - def __init__(self, port=None, **kwargs): - """Create a sangaboard object. - - Arguments are passed to the constructor of - :class:`Sangaboard.extensible_serial_instrument.ExtensibleSerialInstrument`, - most likely the only one necessary is `port` which should be set to the serial port - you will use to communicate with the motor controller. That's the first argument so - it doesn't need to be named. - """ - - # Initialise basic serial instrument with specified - ExtensibleSerialInstrument.__init__(self, port, **kwargs) - - try: - # Make absolutely sure that whatever port we're on is valid - self.check_valid_firmware() - - #Bit messy: Defining all valid modules as not available, then overwriting with available information if available. - self.light_sensor = LightSensor(False) - - for module in self.list_modules(): - module_type=module.split(':')[0].strip() - module_model=module.split(':')[1].strip() - if module_type.startswith('Light Sensor'): - if module_model in self.supported_light_sensors: - self.light_sensor = LightSensor(True,parent=self,model=module_model) - else: - logging.warning("Light sensor model \"%s\" not recognised."%(module_model)) - elif module_type.startswith('Endstops'): - self.endstops = Endstops(True, parent=self, model=module_model) - else: - logging.warning("Module type \"{}\" not recognised.".format(module_type)) - - self.board = self.query("board",timeout=2).rstrip() - - except Exception as e: - # If an error occurred while setting up (e.g. because the board isn't connected or something) - # make sure we close the serial port cleanly (otherwise it hangs open). - self.close() - logging.error(e) - logging.error("You may need to update the firmware running on the Sangaboard.") - raise e - - def test_communications(self): - """ - Overrides superclass, used in self.open(), and port scanning - """ - return self.check_valid_firmware() - - def check_valid_firmware(self): - logging.debug("Running firmware checks") - - # Request firmware version from the board - self.firmware = self.query("version",timeout=2).rstrip() - logging.info("Firmware response: {}".format(self.firmware)) - # Check for valid firmware string - if self.firmware: - match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware) - if not match: - # Try old firmware format - match = re.match(r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware) - if not match: - logging.error("Version string \"{}\" not recognised.".format(self.firmware)) - return False - else: - logging.error("No firmware version string was returned.") - return False - - # Check for matching/valid version number - version = [int(g) for g in match.groups()[1:]] - version_tuple = (version[0], version[1]) - - self.firmware_version = match.group(1) - - if version_tuple not in Sangaboard.valid_firmwares: - logging.error("This version of the Python module requires firmware v0.5 (with legacy support for v0.4)") - return False - - if version_tuple in Sangaboard.deprecated_firmwares: - logging.warning( - "Warning this Sangaboard is using v0.4 of the firmware, this will soon be depreciated! " - "Please consider updating the Sangaboard firmware" - ) - - return True - - def move_rel(self, displacement, axis=None): - """Make a relative move. - - displacement: integer or array/list of 3 integers - axis: None (for 3-axis moves) or one of 'x','y','z' - """ - if axis is not None: - assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names) - self.query("mr{} {}".format(axis, int(displacement))) - else: - #TODO: assert displacement is 3 integers - self.query("mr {} {} {}".format(*list(displacement))) - - def release_motors(self): - """De-energise the stepper motor coils""" - self.query("release") - - def zero(self): - """Zero the stored motor positions""" - self.query("zero") - - def move_abs(self, final, **kwargs): - """Make an absolute move to a position - - NB the sangaboard only accepts relative move commands, so this first - queries the board for its position, then instructs it to make about - relative move. - """ - rel_mov = [f_pos-i_pos for f_pos,i_pos in zip(final,self.position)] - return self.move_rel(rel_mov, **kwargs) - - def query(self, message, *args, **kwargs): - """Send a message and read the response. See ExtensibleSerialInstrument.query()""" - time.sleep(0.001) # This is to protect the stage from us talking too fast! - return ExtensibleSerialInstrument.query(self, message, *args, **kwargs) - - def list_modules(self): - """Return a list of strings detailing optional modules. - - Each module will correspond to a string of the form ``Module Name: Model`` - """ - modules = self.query("list_modules",multiline=True,termination_line="--END--\r\n").split('\r\n')[:-2] - return [str(module) for module in modules] - - def print_help(self): - """Print the stage's built-in help message.""" - print(self.query("help",multiline=True,termination_line="--END--\r\n")) - -class LightSensor(OptionalModule): - """An optional module giving access to the light sensor. - - If a light sensor is enabled in the motor controller's firmware, then - the :class:`sangaboard.Sangaboard` will gain an optional - module which is an instance of this class. It can be used to access - the light sensor (usually via the I2C bus). - """ - valid_gains = None - _valid_gains_int = None - integration_time = QueriedProperty( - get_cmd="light_sensor_integration_time?", - set_cmd="light_sensor_integration_time %d", - response_string="light sensor integration time %d ms", - doc="Get or set the integration time of the light sensor in milliseconds." - ) - intensity = QueriedProperty(get_cmd="light_sensor_intensity?", response_string="%d", - doc="Read the current intensity measured by the light sensor (arbitrary units).") - - def __init__(self,available,parent=None,model="Generic"): - super(LightSensor, self).__init__(available,parent=parent,module_type="LightSensor",model=model) - if available: - self.valid_gains = self.__get_gain_values() - self._valid_gains_int = [int(g) for g in self.valid_gains] - - @property - def gain(self): - """"Get or set the current gain value of the light sensor. - - Valid gain values are defined in the `valid_gains` property, and should be floating-point numbers.""" - self.confirm_available() - gain = self._parent.query('light_sensor_gain?') - M = re.search('[0-9\.]+(?=x)',gain) - assert M is not None, "Cannot read gain string: \"{}\"".format(gain) - #gain is a float as non integer gains exist but are set with floor of value - return float(M.group()) - - @gain.setter - def gain(self,val): - self.confirm_available() - assert int(val) in self._valid_gains_int, "Gain {} not valid must be one of: {}".format(val,self.valid_gains) - gain = self._parent.query('light_sensor_gain %d'%(int(val))) - M = re.search('[0-9\.]+(?=x)',gain) - assert M is not None, "Cannot read gain string: \"{}\"".format(gain) - #gain is a float as non integer gains exist but are set with floor of value - assert int(val) == int(float(M.group())), 'Gain of {} set, \"{}\" returned'.format(val,gain) - - def __get_gain_values(self): - """Read the allowable values for the light sensor's gain. - - This function will attempt to return a list of floating-point numbers which may - be used as values of the `gain` property. If the stage returns non-floating-point - values, the list will be of strings. - """ - self.confirm_available() - gains = self._parent.query('light_sensor_gain_values?') - try: - M = re.findall('[0-9\.]+(?=x)',gains) - return [float(gain) for gain in M] - except: - # Fall back to strings if we don't get floats (unlikely) - gain_strings = gains[20:].split(", ") - return gain_strings - -class Endstops(OptionalModule): - """An optional module for use with endstops. - - If endstops are installed in the firmware the :class:`sangaboard.Sangaboard` - will gain an optional module which is an instance of this class. It can be used to retrieve - the type, state of the endstops, read and write maximum positions, and home. - """ - - installed=[] - """List of installed endstop types (min, max, soft)""" - - def __init__(self,available,parent=None,model="min"): - super(Endstops, self).__init__(available,parent=parent,model="Endstops") - self.installed=model.split(' ') - - status = QueriedProperty(get_cmd="endstops?", response_string=r"%d %d %d", - doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis") - maxima = QueriedProperty(get_cmd="max_p?", set_cmd="max %d %d %d", response_string="%d %d %d", - doc="Vector of maximum positions, homing to max endstops will measure this, "+ - "can be set to a known value for use with max only and min+soft endstops") - - def home(self, direction="min", axes=['x','y','z']): - """ Home given/all axes in the given direction (min/max/both) - - :param direction: one of {min,max,both} - :param axes: list of axes e.g. ['x','y'] - """ - ax=0 - if 'x' in axes: - ax+=1 - if 'y' in axes: - ax+=2 - if 'z' in axes: - ax+=3 - - if direction == "min" or direction == "both": - self._parent.query('home_min {}'.format(ax)) - if direction == "max" or direction == "both": - self._parent.query('home_max {}'.format(ax)) - diff --git a/openflexure_microscope/task.py b/openflexure_microscope/task.py deleted file mode 100644 index e2a9a085..00000000 --- a/openflexure_microscope/task.py +++ /dev/null @@ -1,163 +0,0 @@ -from threading import Thread -import datetime -import logging -import traceback -import uuid - -from openflexure_microscope.exceptions import TaskDeniedException -from openflexure_microscope.utilities import entry_by_id - - -class TaskOrchestrator: - """ - Class responsible for spawning threaded tasks, and storing their returns. - A microscope should contain exactly one instance of `TaskOrchestrator`. - - Attributes: - tasks (list): List of `Task` objects - - """ - def __init__(self): - self.tasks = [] - - @property - def state(self): - """ - Returns a list of dictionary representations of all tasks in the session. - """ - state = {} - - for task in self.tasks: - state[task.id] = task.state - - return state - - def task_from_id(self, task_id: str): - """ - Returns a particular task object with the specified `task_id`. - """ - return entry_by_id(task_id, self.tasks) - - def start(self, function, *args, **kwargs): - """ - Attach and start a new long-running task, if allowed by `start_condition()`. - Returns an instance of :py:class:`openflexure_microscope.task.Task`, which can be used to check the state - or eventual return of the task. - - Args: - function (function): The target function to run in the background - args, kwargs: Arguments that will be passed to `function` at runtime. - """ - # Create a task object - task = Task(function, *args, **kwargs) - - # If the task isn't allowed to run - if not self.start_condition(task): - raise TaskDeniedException("Unable to start this task. Aborting.") - else: - self.tasks.append(task) - task.start() - return task - - def clean(self): - """ - Remove all inactive tasks from the task array. - This will remove tasks that either finished or never started. - """ - self.tasks = [task for task in self.tasks if task._running] - - def delete(self, task_id): - """ - Delete a given task by ID, only if task is not currently running. - """ - success = False - - for task in self.tasks: - if task.id == task_id and not task._running: - self.tasks.remove(task) - success = True - - return success - - def start_condition(self, task_obj): - """ - Method returning bool describing if a particular task is allowed to run. - Currently always allows tasks to start, as locks determine access to hardware. - """ - # return not any([task._running for task in self.tasks]) - return True - - -class Task: - """ - Class responsible for running a task function in a thread, and handling return or errors. - Tasks should be created by an instance of :py:class:`openflexure_microscope.task.TaskOrchestrator`. - - Args: - function (function): Function to be called in the task thread. - - Attributes: - task (function): Function to be called in the task thread. - args: Positional arguments to be passed to the task function - kwargs: Keyword arguments to be passed to the task function. - _running (bool): If task is currently running - id (str): Unique ID for the task - state (dict): Dictionary describing the full state of the task. - `status` will be one of 'idle'', 'running', 'error', or 'success'. - `return` eventually holds the return value of the task function. - """ - def __init__(self, function, *args, **kwargs): - # The task long-running method - self.task = function - self.args = args - self.kwargs = kwargs - - self._running = False - - self.id = uuid.uuid4().hex - - self.state = { - 'id': self.id, - 'status': 'idle', - 'return': None, - 'start_time': None, - 'end_time': None, - } - - def process(self, f): - """ - Wraps the target function to handle recording `status` and `return` to `state`. - """ - def wrapped(*args, **kwargs): - self.state['status'] = 'running' - try: - r = f(*args, **kwargs) - s = 'success' - except Exception as e: - logging.error(e) - logging.error(traceback.format_exc()) - r = str(e) - s = 'error' - self.state['return'] = r - self.state['status'] = s - return wrapped - - def run(self): - """ - Records the task start and end times to `state`, and runs the task wrapped in `process`. - """ - self.state['start_time'] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") - self.process(self.task)(*self.args, **self.kwargs) - self._running = False - self.state['end_time'] = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S") - - def start(self): # Start and draw - """ - Spawns a thread, and starts `run()` in that thread. - """ - # If not already running - if not self._running: - self._running = True # Set start command - thread = Thread(target=self.run) # Define thread - thread.daemon = True # Stop this thread when main thread closes - thread.start() # Start thread diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index de2503a0..3ca3f943 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -1,8 +1,59 @@ +import re import copy import operator +import base64 +from uuid import UUID +import numpy as np +import logging +import time +from collections import abc from functools import reduce from contextlib import contextmanager +class Timer(object): + def __init__(self, name): + self.name = name + self.start = None + self.end = None + def __enter__(self): + self.start = time.time() + def __exit__(self, type, value, traceback): + self.end = time.time() + logging.debug(f"{self.name} time: {self.end - self.start}") + + +def deserialise_array_b64(b64_string, dtype, shape): + flat_arr = np.fromstring(base64.b64decode(b64_string), dtype) + return flat_arr.reshape(shape) + + +def serialise_array_b64(npy_arr): + b64_string = base64.b64encode(npy_arr).decode("ascii") + dtype = str(npy_arr.dtype) + shape = npy_arr.shape + return b64_string, dtype, shape + + +def ndarray_to_json(arr: np.ndarray): + if isinstance(arr, memoryview): + # We can transparently convert memoryview objects to arrays + # This comes in very handy for the lens shading table. + arr = np.array(arr) + b64_string, dtype, shape = serialise_array_b64(arr) + return {"@type": "ndarray", "dtype": dtype, "shape": shape, "base64": b64_string} + + +def json_to_ndarray(json_dict: dict): + if not json_dict.get("@type") != "ndarray": + logging.warning("No valid @type attribute found. Conversion may fail.") + for required_param in ("dtype", "shape", "base64"): + if not json_dict.get(required_param): + raise KeyError(f"Missing required key {required_param}") + + return deserialise_array_b64( + json_dict.get("base64"), json_dict.get("dtype"), json_dict.get("shape") + ) + @contextmanager def set_properties(obj, **kwargs): @@ -16,7 +67,11 @@ def set_properties(obj, **kwargs): try: saved_properties[k] = getattr(obj, k) except AttributeError: - print("Warning: could not get {} on {}. This property will not be restored!".format(k, obj)) + print( + "Warning: could not get {} on {}. This property will not be restored!".format( + k, obj + ) + ) for k, v in kwargs.items(): setattr(obj, k, v) try: @@ -26,12 +81,14 @@ def set_properties(obj, **kwargs): setattr(obj, k, v) -def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=None, asint=True): +def axes_to_array( + coordinate_dictionary, axis_keys=("x", "y", "z"), base_array=None, asint=True +): """Takes key-value pairs of a JSON value, and maps onto an array""" # If no base array is given if not base_array: # Create an array of zeros - base_array = [0]*len(axis_keys) + base_array = [0] * len(axis_keys) else: # Create a copy of the passed base_array base_array = copy.copy(base_array) @@ -39,7 +96,9 @@ def axes_to_array(coordinate_dictionary, axis_keys=('x', 'y', 'z'), base_array=N # Do the mapping for axis, key in enumerate(axis_keys): if key in coordinate_dictionary: - base_array[axis] = int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key] + base_array[axis] = ( + int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key] + ) return base_array @@ -54,10 +113,42 @@ def filter_dict(dictionary: dict, keys: list): return out -def entry_by_id(entry_id: str, object_list: list): +def entry_by_uuid(entry_id: str, object_list: list): """Return an object from a list, if .id matches id argument.""" found = None + if type(entry_id) == str: + converter = str + elif type(entry_id) == int: + converter = int + elif isinstance(entry_id, UUID): + converter = int + else: + raise TypeError("Argument entry_id must be a string, integer, or UUID object.") for o in object_list: - if o.id == entry_id: + # Convert to strings (in case of UUID objects, for example) + if converter(o.id) == converter(entry_id): found = o return found + + +def recursively_apply(data, func): + """ + Recursively apply a function to a dictionary, list, array, or tuple + + Args: + data: Input iterable data + func: Function to apply to all non-iterable values + """ + # If the object is a dictionary + if isinstance(data, abc.Mapping): + return {key: recursively_apply(val, func) for key, val in data.items()} + # If the object is iterable but NOT a dictionary or a string + elif ( + isinstance(data, abc.Iterable) + and not isinstance(data, abc.Mapping) + and not isinstance(data, str) + ): + return [recursively_apply(x, func) for x in data] + # if the object is neither a map nor iterable + else: + return func(data) diff --git a/poetry.lock b/poetry.lock index 2c9d78f1..f9fec63e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,24 +6,111 @@ 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.1" + +[package.extras] +dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["marshmallow (>=2.19.2)", "pyyaml (5.3.1)", "sphinx (3.0.4)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"] +lint = ["flake8 (3.8.2)", "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 = "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 = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + [[package]] category = "dev" description = "Internationalization utilities" name = "babel" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.7.0" +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 = "18.9b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=17.4.0" +click = ">=6.5" +toml = ">=0.9.4" + +[package.extras] +d = ["aiohttp (>=3.3.2)"] + +[[package]] +category = "main" +description = "Calibration and mapping between stage and camera coordinates in a microscope" +name = "camera-stage-mapping" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.1.3" + +[package.dependencies] +numpy = ">=1.17,<2.0" + +[package.extras] +all = ["opencv-python-headless (>=4.1,<5.0)", "scipy (>=1.4,<2.0)"] +correlation = ["opencv-python-headless (>=4.1,<5.0)", "scipy (>=1.4,<2.0)"] + [[package]] category = "dev" description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2019.6.16" +version = "2020.6.20" [[package]] category = "dev" @@ -38,8 +125,8 @@ category = "main" description = "Composable command line interface toolkit" name = "click" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "7.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" [[package]] category = "dev" @@ -47,31 +134,47 @@ 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.*" -version = "0.4.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" [[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 = "Dictionary with auto-expiring values for caching purposes" +name = "expiringdict" +optional = false python-versions = "*" -version = "0.14" +version = "1.2.1" + +[package.extras] +tests = ["dill", "coverage", "coveralls", "mock", "nose"] [[package]] category = "main" description = "A simple framework for building complex web applications." name = "flask" optional = false -python-versions = "*" -version = "1.0.3" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.1.2" [package.dependencies] -Jinja2 = ">=2.10" -Werkzeug = ">=0.14" +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" @@ -84,13 +187,25 @@ version = "3.0.8" Flask = ">=0.9" Six = "*" +[[package]] +category = "main" +description = "Barebones websocket extension for Flask, using Pythonthreading for low-traffic concurrency" +name = "flask-threaded-sockets" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.2.0" + +[package.dependencies] +flask = ">=1.1.2,<2.0.0" +werkzeug = ">=1.0.1,<2.0.0" + [[package]] category = "main" description = "Clean single-source support for Python 3 and 2" name = "future" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.17.1" +version = "0.18.2" [[package]] category = "dev" @@ -98,7 +213,15 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8" +version = "2.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" @@ -106,7 +229,21 @@ 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.1.0" +version = "1.2.0" + +[[package]] +category = "dev" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "4.3.21" + +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] [[package]] category = "main" @@ -118,15 +255,43 @@ version = "1.1.0" [[package]] category = "main" -description = "A small but fast and easy to use stand-alone template engine written in pure python." +description = "A very fast and expressive template engine." name = "jinja2" optional = false -python-versions = "*" -version = "2.10.1" +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 = "main" +description = "Python implementation of LabThings, based on the Flask microframework" +name = "labthings" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.8.0" + +[package.dependencies] +Flask = ">=1.1.1,<2.0.0" +apispec = ">=3.2.0,<4.0.0" +flask-cors = ">=3.0.8,<4.0.0" +flask-threaded-sockets = ">=0.2.0,<0.3.0" +marshmallow = ">=3.4.0,<4.0.0" +webargs = ">=6.0.0,<7.0.0" +zeroconf = ">=0.24.5,<0.29.0" + +[[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." @@ -137,24 +302,57 @@ version = "1.1.1" [[package]] category = "main" -description = "NumPy is the fundamental package for array computing with Python." -name = "numpy" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +name = "marshmallow" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.16.4" +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 = "main" -description = "Control scripts for the OpenFlexure Nano Motor Controller" -name = "openflexure-stage" +description = "NumPy is the fundamental package for array computing with Python." +name = "numpy" +optional = false +python-versions = ">=3.5" +version = "1.18.2" + +[[package]] +category = "main" +description = "Wrapper package for OpenCV python bindings." +marker = "python_version >= \"3.7\" and python_version < \"3.8\"" +name = "opencv-python-headless" optional = false python-versions = "*" -version = "0.2.1" +version = "4.1.0.25" [package.dependencies] -future = "*" -numpy = "*" -pyserial = "*" +numpy = ">=1.11.1" + +[[package]] +category = "main" +description = "Wrapper package for OpenCV python bindings." +marker = "python_version >= \"3.8\" and python_version < \"4.0\"" +name = "opencv-python-headless" +optional = false +python-versions = "*" +version = "4.2.0.34" + +[package.dependencies] +numpy = ">=1.11.1" [[package]] category = "dev" @@ -162,7 +360,7 @@ description = "Core utilities for Python packages" name = "packaging" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.0" +version = "20.4" [package.dependencies] pyparsing = ">=2.0.2" @@ -174,12 +372,17 @@ description = "A pure Python interface for the Raspberry Pi camera module." name = "picamera" optional = false python-versions = "*" -version = "1.13.1b0" +version = "1.13.2b0" + +[package.extras] +array = ["numpy"] +doc = ["sphinx"] +test = ["coverage", "pytest", "mock", "pillow", "numpy"] [package.source] -reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb" -type = "git" -url = "https://github.com/rwb27/picamera.git" +reference = "" +type = "url" +url = "https://github.com/rwb27/picamera/releases/download/v1.13.2b0/picamera-1.13.2b0-py3-none-any.whl" [[package]] category = "main" @@ -189,13 +392,52 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "5.4.1" +[[package]] +category = "main" +description = "Cross-platform lib for process and system monitoring in Python." +name = "psutil" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.7.2" + +[package.extras] +test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] + [[package]] category = "dev" description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.4.2" +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.5.3" + +[package.dependencies] +astroid = ">=2.4.0,<=2.5" +colorama = "*" +isort = ">=4.2.5,<5" +mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" + +[[package]] +category = "main" +description = "Python interface to your NPM and package.json." +name = "pynpm" +optional = false +python-versions = "*" +version = "0.1.2" + +[package.extras] +all = ["Sphinx (>=1.5.1)", "check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)"] +docs = ["Sphinx (>=1.5.1)"] +tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)"] [[package]] category = "dev" @@ -203,7 +445,7 @@ description = "Python parsing module" name = "pyparsing" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.0" +version = "2.4.7" [[package]] category = "main" @@ -213,21 +455,24 @@ optional = false python-versions = "*" version = "3.4" +[[package]] +category = "main" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" + +[package.dependencies] +six = ">=1.5" + [[package]] category = "dev" description = "World timezone definitions, modern and historical" name = "pytz" optional = false python-versions = "*" -version = "2019.1" - -[[package]] -category = "main" -description = "YAML parser and emitter for Python" -name = "pyyaml" -optional = false -python-versions = "*" -version = "3.13" +version = "2020.1" [[package]] category = "dev" @@ -235,14 +480,18 @@ description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.22.0" +version = "2.24.0" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + [[package]] category = "dev" description = "a python refactoring library..." @@ -255,9 +504,22 @@ version = "0.14.0" category = "main" description = "A module to control Raspberry Pi GPIO channels" name = "rpi.gpio" -optional = true +optional = false python-versions = "*" -version = "0.6.5" +version = "0.7.0" + +[[package]] +category = "main" +description = "Communication to the Sangaboard unipolar motor driver" +name = "sangaboard" +optional = false +python-versions = "*" +version = "0.2.3" + +[package.dependencies] +future = "*" +numpy = "*" +pyserial = "*" [[package]] category = "main" @@ -265,7 +527,7 @@ description = "SciPy: Scientific Library for Python" name = "scipy" optional = false python-versions = ">=3.5" -version = "1.3.0" +version = "1.4.1" [package.dependencies] numpy = ">=1.13.3" @@ -275,16 +537,16 @@ category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.12.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" [[package]] category = "dev" -description = "This package provides 23 stemmers for 22 languages generated from Snowball algorithms." +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." name = "snowballstemmer" optional = false python-versions = "*" -version = "1.9.0" +version = "2.0.0" [[package]] category = "dev" @@ -292,13 +554,13 @@ description = "Python documentation generator" name = "sphinx" optional = false python-versions = ">=3.5" -version = "2.1.2" +version = "3.1.2" [package.dependencies] Jinja2 = ">=2.3" Pygments = ">=2.0" alabaster = ">=0.7,<0.8" -babel = ">=1.3,<2.0 || >2.0" +babel = ">=1.3" colorama = ">=0.3.5" docutils = ">=0.12" imagesize = "*" @@ -313,29 +575,46 @@ 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 = "" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" name = "sphinxcontrib-applehelp" optional = false -python-versions = "*" -version = "1.0.1" +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] [[package]] category = "dev" -description = "" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." name = "sphinxcontrib-devhelp" optional = false -python-versions = "*" -version = "1.0.1" +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] [[package]] category = "dev" -description = "" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" name = "sphinxcontrib-htmlhelp" optional = false -python-versions = "*" -version = "1.0.2" +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] [[package]] category = "dev" @@ -357,81 +636,572 @@ optional = false python-versions = ">=3.5" version = "1.0.1" -[[package]] -category = "dev" -description = "" -name = "sphinxcontrib-qthelp" -optional = false -python-versions = "*" -version = "1.0.2" +[package.extras] +test = ["pytest", "flake8", "mypy"] [[package]] category = "dev" -description = "" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +name = "sphinxcontrib-qthelp" +optional = false +python-versions = ">=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 = "1.1.3" +version = "0.10.1" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +marker = "implementation_name == \"cpython\" and python_version < \"3.8\"" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.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.*, <4" -version = "1.25.3" +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.*" -version = "0.15.4" +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.0" + +[package.dependencies] +ifaddr = ">=0.1.7" + +[extras] +rpi = ["RPi.GPIO"] [metadata] -content-hash = "c2fee81479442877ae94095d15f05018dc5290eee42edeb7d79d067fdf0ffab6" -python-versions = "^3.5" +content-hash = "446a192e461cdf5d1f12697f3c23d2a1b3cd9250cd6e14ccc884a14d674f8f19" +python-versions = "^3.6" -[metadata.hashes] -alabaster = ["446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", "a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"] -babel = ["af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", "e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"] -certifi = ["046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", "945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"] -chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] -colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] -docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] -flask = ["ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3", "e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"] -flask-cors = ["72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", "f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"] -future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] -idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] -imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"] -itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"] -jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] -markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] -numpy = ["0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", "141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", "14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", "27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", "2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", "3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", "52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", "6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", "7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", "7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", "94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", "a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", "ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", "b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", "b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", "cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", "d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", "dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", "dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", "e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", "ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", "f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", "f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"] -openflexure-stage = ["2dd7d0d30c9327a1ed108bda3d97fb0d75137ed545045a9bb2112a9763fe9703", "a76758c957f34311d7187254268665f36f8b7697c9118e2cac4942a8dd572a8d"] -packaging = ["0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", "9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"] +[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.1-py2.py3-none-any.whl", hash = "sha256:b65063e11968a8c26dbbe9b4100ee24026a41cc358dd204df279bdedd7d8dea2"}, + {file = "apispec-3.3.1.tar.gz", hash = "sha256:f5244ccca33f7a81309f6b3c9d458e33e869050c2d861b9f8cee24b3ad739d2b"}, +] +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"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] +black = [ + {file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"}, + {file = "black-18.9b0.tar.gz", hash = "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"}, +] +camera-stage-mapping = [ + {file = "camera-stage-mapping-0.1.3.tar.gz", hash = "sha256:048dfd465ca04b9baa7714b4ce8d53a97eecaa41781cf07e4bd8649e6d4d97d0"}, + {file = "camera_stage_mapping-0.1.3-py3-none-any.whl", hash = "sha256:16d77f550067aa3e6bcf9de3f39d0ba2a4581927aa729d83e5f0e46ac6c81d5d"}, +] +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"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +expiringdict = [ + {file = "expiringdict-1.2.1.tar.gz", hash = "sha256:fe2ba427220425c3c8a3d29f6d2e2985bcee323f8bcd4021e68ebefbd90d8250"}, +] +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.8.tar.gz", hash = "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16"}, + {file = "Flask_Cors-3.0.8-py2.py3-none-any.whl", hash = "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"}, +] +flask-threaded-sockets = [ + {file = "flask_threaded_sockets-0.2.0-py3-none-any.whl", hash = "sha256:e3fc55ccf7f22e801b967f15efd4d30ae8627c1fdc3c27e0195ba1bd369e32e1"}, + {file = "flask_threaded_sockets-0.2.0.tar.gz", hash = "sha256:e9d2ee66ae1c9c2ac86373510c2a4295e0230109130b7bc6d2499f5cd4140361"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +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"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +itsdangerous = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +labthings = [ + {file = "labthings-0.8.0-py3-none-any.whl", hash = "sha256:133eb1480dc6ea851faa9a16bcf34d075d513935eac82f8da492e6dc5e336b6c"}, + {file = "labthings-0.8.0.tar.gz", hash = "sha256:3c440c1bb9547858f2dbe6a85746174c6050016a0a86d773d986ca83fbf89874"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +marshmallow = [ + {file = "marshmallow-3.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"}, +] +numpy = [ + {file = "numpy-1.18.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a1baa1dc8ecd88fb2d2a651671a84b9938461e8a8eed13e2f0a812a94084d1fa"}, + {file = "numpy-1.18.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a244f7af80dacf21054386539699ce29bcc64796ed9850c99a34b41305630286"}, + {file = "numpy-1.18.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6fcc5a3990e269f86d388f165a089259893851437b904f422d301cdce4ff25c8"}, + {file = "numpy-1.18.2-cp35-cp35m-win32.whl", hash = "sha256:b5ad0adb51b2dee7d0ee75a69e9871e2ddfb061c73ea8bc439376298141f77f5"}, + {file = "numpy-1.18.2-cp35-cp35m-win_amd64.whl", hash = "sha256:87902e5c03355335fc5992a74ba0247a70d937f326d852fc613b7f53516c0963"}, + {file = "numpy-1.18.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9ab21d1cb156a620d3999dd92f7d1c86824c622873841d6b080ca5495fa10fef"}, + {file = "numpy-1.18.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cdb3a70285e8220875e4d2bc394e49b4988bdb1298ffa4e0bd81b2f613be397c"}, + {file = "numpy-1.18.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6d205249a0293e62bbb3898c4c2e1ff8a22f98375a34775a259a0523111a8f6c"}, + {file = "numpy-1.18.2-cp36-cp36m-win32.whl", hash = "sha256:a35af656a7ba1d3decdd4fae5322b87277de8ac98b7d9da657d9e212ece76a61"}, + {file = "numpy-1.18.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1598a6de323508cfeed6b7cd6c4efb43324f4692e20d1f76e1feec7f59013448"}, + {file = "numpy-1.18.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:deb529c40c3f1e38d53d5ae6cd077c21f1d49e13afc7936f7f868455e16b64a0"}, + {file = "numpy-1.18.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd77d58fb2acf57c1d1ee2835567cd70e6f1835e32090538f17f8a3a99e5e34b"}, + {file = "numpy-1.18.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b1fe1a6f3a6f355f6c29789b5927f8bd4f134a4bd9a781099a7c4f66af8850f5"}, + {file = "numpy-1.18.2-cp37-cp37m-win32.whl", hash = "sha256:2e40be731ad618cb4974d5ba60d373cdf4f1b8dcbf1dcf4d9dff5e212baf69c5"}, + {file = "numpy-1.18.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4ba59db1fcc27ea31368af524dcf874d9277f21fd2e1f7f1e2e0c75ee61419ed"}, + {file = "numpy-1.18.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59ca9c6592da581a03d42cc4e270732552243dc45e87248aa8d636d53812f6a5"}, + {file = "numpy-1.18.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1b0ece94018ae21163d1f651b527156e1f03943b986188dd81bc7e066eae9d1c"}, + {file = "numpy-1.18.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:82847f2765835c8e5308f136bc34018d09b49037ec23ecc42b246424c767056b"}, + {file = "numpy-1.18.2-cp38-cp38-win32.whl", hash = "sha256:5e0feb76849ca3e83dd396254e47c7dba65b3fa9ed3df67c2556293ae3e16de3"}, + {file = "numpy-1.18.2-cp38-cp38-win_amd64.whl", hash = "sha256:ba3c7a2814ec8a176bb71f91478293d633c08582119e713a0c5351c0f77698da"}, + {file = "numpy-1.18.2.zip", hash = "sha256:e7894793e6e8540dbeac77c87b489e331947813511108ae097f1715c018b8f3d"}, +] +opencv-python-headless = [ + {file = "opencv_python_headless-4.1.0.25-cp27-cp27m-macosx_10_7_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a71b5bdb39c99d706493b5dc4fd7cb803ece6ac54b9115ac7aa3c4dfb1338348"}, + {file = "opencv_python_headless-4.1.0.25-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:84ed23093b5da546ae0adf36182d3893940ea1f8ba38ea84f6d13ee9220bcff8"}, + {file = "opencv_python_headless-4.1.0.25-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e3aa46a96c5b65eab349266aecfcc3297f3ac59b86314c1507558dcb978baa02"}, + {file = "opencv_python_headless-4.1.0.25-cp27-cp27m-win32.whl", hash = "sha256:13a9746459029c0bd64713ec81b5a57aad6e1a5dc672ee1e44708a46f5f099f6"}, + {file = "opencv_python_headless-4.1.0.25-cp27-cp27m-win_amd64.whl", hash = "sha256:ac059ce7c4a8162b208d403c4418ea951e354416339ad6859b47ddc80ba9d0a8"}, + {file = "opencv_python_headless-4.1.0.25-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:40fcb7de9d92b1b69e03f7de9d5d976f2cbd34eabe7df3f12c41d525e51b49d7"}, + {file = "opencv_python_headless-4.1.0.25-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:af4ccd244398d7cee5f416591b8672ae38c4da278bd89b449c0ee4d1d8391e03"}, + {file = "opencv_python_headless-4.1.0.25-cp34-cp34m-macosx_10_7_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:2444b3f351182f0707cafab76b935134a2ee94579e669b7dae14648acd243e93"}, + {file = "opencv_python_headless-4.1.0.25-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:fd119b75ceec1e64a3ff9bad0096445851bc8e3ba89cdccc11cf80db2244c2df"}, + {file = "opencv_python_headless-4.1.0.25-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:b31f50e6dfbeaa40164630dd3b4f3274d0378da39c6ad85e172cecc66b4074b4"}, + {file = "opencv_python_headless-4.1.0.25-cp34-cp34m-win32.whl", hash = "sha256:2504f27f53d419d2dbffb0390d512d666bf9aa0eb66114b4b81e1ed266282e8e"}, + {file = "opencv_python_headless-4.1.0.25-cp34-cp34m-win_amd64.whl", hash = "sha256:6e1f82e8a8d171bcb3dec08404e824aaedca9f12d9b957893c3c0f76d7aa5243"}, + {file = "opencv_python_headless-4.1.0.25-cp35-cp35m-macosx_10_7_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:8691c1b8059ad760f776eb7b87b1ec1202832ef0938497f5e8b8c857ea3ed8ce"}, + {file = "opencv_python_headless-4.1.0.25-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:536b3f25cbe832a89dd84e526a6f7a262cb98d9b4fb566bf63438b7165979abc"}, + {file = "opencv_python_headless-4.1.0.25-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:94e84603a0f3dbf869fb166d0ec086adfcbaa8f97957d8b68b9788168d1462d4"}, + {file = "opencv_python_headless-4.1.0.25-cp35-cp35m-win32.whl", hash = "sha256:408428eb7c7d8075b915ba86012177cedc4018bc3ff69b018f33042a86c8f208"}, + {file = "opencv_python_headless-4.1.0.25-cp35-cp35m-win_amd64.whl", hash = "sha256:5193fd3608ac9e6382df74c04016bdd55ff913328660b4e2419508fc0e88386e"}, + {file = "opencv_python_headless-4.1.0.25-cp36-cp36m-macosx_10_7_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:968587e6f251128830fd449b87829e8be1d7d35d5263c7a6d2698a58dd8e84ff"}, + {file = "opencv_python_headless-4.1.0.25-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:57a9815214a420180471521265687413e057e8ee19fa3f30ca5d5f4ac1f41ffa"}, + {file = "opencv_python_headless-4.1.0.25-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3dacdf4c5f9b17bd0e763c0e4399bc516bc9027ef6fac2dd1b2547a4f32ceefc"}, + {file = "opencv_python_headless-4.1.0.25-cp36-cp36m-win32.whl", hash = "sha256:3a30ad348e8cfcab5ded9e927220bc07b18d4a26ebde20c323a35054693d91ae"}, + {file = "opencv_python_headless-4.1.0.25-cp36-cp36m-win_amd64.whl", hash = "sha256:905c08a483137f2edd362d46420bc1c88f026b4889840b1af8b43ad29e7c2a9c"}, + {file = "opencv_python_headless-4.1.0.25-cp37-cp37m-macosx_10_7_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:55a8361257c21272b08d782c7847754c664861472c72f07862c4880a6dcc1ae8"}, + {file = "opencv_python_headless-4.1.0.25-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c8b64eaca2bd59c15db31e30cbe02151162525e855ebd79efa66d789ac0f9191"}, + {file = "opencv_python_headless-4.1.0.25-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7709abc1c1341b831bf30d2b2afaa4877bdcab7dd775c94864608c08067e65b9"}, + {file = "opencv_python_headless-4.1.0.25-cp37-cp37m-win32.whl", hash = "sha256:3020c6a996cea3a8d36ef5c49c8641a8f32a2bb77944128749af756de2946d6e"}, + {file = "opencv_python_headless-4.1.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:5f1f5b0325cfd50cd58fc64bf66feed56238305e41450f04ab450092e18ee95b"}, + {file = "opencv_python_headless-4.2.0.34-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:45f90a872bc2e3d3257e06508ea18b3e2d429cf1b20de1a8f5c57251cc39fae4"}, + {file = "opencv_python_headless-4.2.0.34-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1641d82b94ce496e6f3df8f2a337eae94cd608e0e61dbb4b697577438d88c258"}, + {file = "opencv_python_headless-4.2.0.34-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3aadcd93f1ad9a92dbfbfdeacb7ffca115f137aa2b07d36495fd141f2a553479"}, + {file = "opencv_python_headless-4.2.0.34-cp35-cp35m-win32.whl", hash = "sha256:cef7e844f5fd73cb602b8ba3f048a68dc2c59b7abb55ca99cceee9f688e909a4"}, + {file = "opencv_python_headless-4.2.0.34-cp35-cp35m-win_amd64.whl", hash = "sha256:70117cd51b158f7628cf2dab24805df40173c3794ca3dfeec22defb326ec0173"}, + {file = "opencv_python_headless-4.2.0.34-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4c3004573ecfbdf46785682a294923661903eb18e4c188106998ef5be255df1f"}, + {file = "opencv_python_headless-4.2.0.34-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:64292b593367e5b37556900b132861d94c05f616d8569c5625b02be6d51e13f7"}, + {file = "opencv_python_headless-4.2.0.34-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ffe74fc421fc77b649f4b762e115fd2b9cea9ba86b4590f7939bd15f92f11f80"}, + {file = "opencv_python_headless-4.2.0.34-cp36-cp36m-win32.whl", hash = "sha256:ed5c7d2fd8dc08ebcb9f8ed09883e3a083a5e0bc35f863ac280f61cec9560b09"}, + {file = "opencv_python_headless-4.2.0.34-cp36-cp36m-win_amd64.whl", hash = "sha256:28b28e3462500dccf75ac5efb2b885494d050fb6444fee37b53f9f950bccca57"}, + {file = "opencv_python_headless-4.2.0.34-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4e9de88b1376f904158698c3335f184cbcdc68eda018eb8f751e27e7407dddd1"}, + {file = "opencv_python_headless-4.2.0.34-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:edfaeea04982166a02c87b85ed5451b77485c86db7a4b216a42e6b686e1b5f1a"}, + {file = "opencv_python_headless-4.2.0.34-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aca7419cae1390a8f2d6776571d1d19db66dadbff2ded79e4ff4bad5d9df6633"}, + {file = "opencv_python_headless-4.2.0.34-cp37-cp37m-win32.whl", hash = "sha256:1df0adfc1b893615ab8487cd207aae356936b0ba1823520e2292c928853c2930"}, + {file = "opencv_python_headless-4.2.0.34-cp37-cp37m-win_amd64.whl", hash = "sha256:67c6ff682e413db759f49a206bafa5ebb8b01dfdefa5199df32741b58772c3d7"}, + {file = "opencv_python_headless-4.2.0.34-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:904f0b4a358b3260d7572956026bf6a0fa0cda31d5a6afeed3e55341a3f36c8d"}, + {file = "opencv_python_headless-4.2.0.34-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f93bec89d943ca1877c38920e934ed1d6193cdc6ff09cb910c19c15643ee07df"}, + {file = "opencv_python_headless-4.2.0.34-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ca9a5f4b04f2b423892ef5934fdd685edd0bc458c77fc7e164156a50e6be0377"}, + {file = "opencv_python_headless-4.2.0.34-cp38-cp38-win32.whl", hash = "sha256:06a96ec4866aa40c68c255042ea0ab8a96c07f2eb54f5b0dc6b8ac08b00c1b7e"}, + {file = "opencv_python_headless-4.2.0.34-cp38-cp38-win_amd64.whl", hash = "sha256:20c450943acebd9aa04566fdef4a658b94eec80a0c39a8f480c9aa1696034fcb"}, +] +packaging = [ + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, +] picamera = [] -pillow = ["01a501be4ae05fd714d269cb9c9f145518e58e73faa3f140ddb67fae0c2607b1", "051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", "07c35919f983c2c593498edcc126ad3a94154184899297cc9d27a6587672cbaa", "0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", "0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", "0cf0208500df8d0c3cad6383cd98a2d038b0678fd4f777a8f7e442c5faeee81d", "163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", "18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", "24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", "267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", "3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", "39fbd5d62167197318a0371b2a9c699ce261b6800bb493eadde2ba30d868fe8c", "4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d", "4baab2d2da57b0d9d544a2ce0f461374dd90ccbcf723fe46689aff906d43a964", "4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", "4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", "505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", "52e2e56fc3706d8791761a157115dc8391319720ad60cc32992350fda74b6be2", "5337ac3280312aa065ed0a8ec1e4b6142e9f15c31baed36b5cd964745853243f", "5ccd97e0f01f42b7e35907272f0f8ad2c3660a482d799a0c564c7d50e83604d4", "5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", "634209852cc06c0c1243cc74f8fdc8f7444d866221de51125f7b696d775ec5ca", "75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522", "7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", "801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", "825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", "87fe838f9dac0597f05f2605c0700b1926f9390c95df6af45d83141e0c514bd9", "9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", "a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", "a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", "a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", "a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", "ac036b6a6bac7010c58e643d78c234c2f7dc8bb7e591bd8bc3555cf4b1527c28", "b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", "ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", "ba6ef2bd62671c7fb9cdb3277414e87a5cd38b86721039ada1464f7452ad30b2", "c8939dba1a37960a502b1a030a4465c46dd2c2bca7adf05fa3af6bea594e720e", "cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", "cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", "d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", "d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", "db418635ea20528f247203bf131b40636f77c8209a045b89fa3badb89e1fcea0", "e1555d4fda1db8005de72acf2ded1af660febad09b4708430091159e8ae1963e", "e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", "e9f13711780c981d6eadd6042af40e172548c54b06266a1aabda7de192db0838", "f0e3288b92ca5dbb1649bd00e80ef652a72b657dc94989fa9c348253d179054b", "f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", "f62b1aeb5c2ced8babd4fbba9c74cbef9de309f5ed106184b12d9778a3971f15", "f71ff657e63a9b24cac254bb8c9bd3c89c7a1b5e00ee4b3997ca1c18100dac28", "fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"] -pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] -pyparsing = ["1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", "9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"] -pyserial = ["6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", "e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"] -pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"] -pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"] -requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] -rope = ["6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", "f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"] -"rpi.gpio" = ["a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"] -scipy = ["03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351", "09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e", "10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e", "1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba", "409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb", "4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340", "6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1", "826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7", "a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae", "a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06", "adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef", "b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785", "c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d", "c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a", "c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921", "db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"] -six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] -snowballstemmer = ["9f3b9ffe0809d174f7047e121431acf99c89a7040f0ca84f94ba53a498e6d0c9"] -sphinx = ["22538e1bbe62b407cf5a8aabe1bb15848aa66bb79559f42f5202bbce6b757a69", "f9a79e746b87921cabc3baa375199c6076d1270cee53915dbd24fdbeaaacc427"] -sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"] -sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"] -sphinxcontrib-htmlhelp = ["4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"] -sphinxcontrib-httpdomain = ["1fb5375007d70bf180cdd1c79e741082be7aa2d37ba99efe561e1c2e3f38191e", "ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"] -sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"] -sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"] -sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"] -urllib3 = ["b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", "dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"] -werkzeug = ["865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c", "a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"] +pillow = [ + {file = "Pillow-5.4.1-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1"}, + {file = "Pillow-5.4.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7"}, + {file = "Pillow-5.4.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e"}, + {file = "Pillow-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c"}, + {file = "Pillow-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f"}, + {file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c"}, + {file = "Pillow-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc"}, + {file = "Pillow-5.4.1-cp34-cp34m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1"}, + {file = "Pillow-5.4.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8"}, + {file = "Pillow-5.4.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055"}, + {file = "Pillow-5.4.1-cp34-cp34m-win32.whl", hash = "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6"}, + {file = "Pillow-5.4.1-cp34-cp34m-win_amd64.whl", hash = "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf"}, + {file = "Pillow-5.4.1-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239"}, + {file = "Pillow-5.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3"}, + {file = "Pillow-5.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697"}, + {file = "Pillow-5.4.1-cp35-cp35m-win32.whl", hash = "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7"}, + {file = "Pillow-5.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe"}, + {file = "Pillow-5.4.1-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca"}, + {file = "Pillow-5.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"}, + {file = "Pillow-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad"}, + {file = "Pillow-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a"}, + {file = "Pillow-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356"}, + {file = "Pillow-5.4.1-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1"}, + {file = "Pillow-5.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9"}, + {file = "Pillow-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3"}, + {file = "Pillow-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f"}, + {file = "Pillow-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3"}, + {file = "Pillow-5.4.1-pp260-pypy_41-win32.whl", hash = "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494"}, + {file = "Pillow-5.4.1-pp360-pp360-win32.whl", hash = "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000"}, + {file = "Pillow-5.4.1-py2.7-macosx-10.13-x86_64.egg", hash = "sha256:634209852cc06c0c1243cc74f8fdc8f7444d866221de51125f7b696d775ec5ca"}, + {file = "Pillow-5.4.1-py2.7-win-amd64.egg", hash = "sha256:0cf0208500df8d0c3cad6383cd98a2d038b0678fd4f777a8f7e442c5faeee81d"}, + {file = "Pillow-5.4.1-py2.7-win32.egg", hash = "sha256:f71ff657e63a9b24cac254bb8c9bd3c89c7a1b5e00ee4b3997ca1c18100dac28"}, + {file = "Pillow-5.4.1-py3.4-win-amd64.egg", hash = "sha256:01a501be4ae05fd714d269cb9c9f145518e58e73faa3f140ddb67fae0c2607b1"}, + {file = "Pillow-5.4.1-py3.4-win32.egg", hash = "sha256:4baab2d2da57b0d9d544a2ce0f461374dd90ccbcf723fe46689aff906d43a964"}, + {file = "Pillow-5.4.1-py3.5-win-amd64.egg", hash = "sha256:f62b1aeb5c2ced8babd4fbba9c74cbef9de309f5ed106184b12d9778a3971f15"}, + {file = "Pillow-5.4.1-py3.5-win32.egg", hash = "sha256:e9f13711780c981d6eadd6042af40e172548c54b06266a1aabda7de192db0838"}, + {file = "Pillow-5.4.1-py3.6-win-amd64.egg", hash = "sha256:07c35919f983c2c593498edcc126ad3a94154184899297cc9d27a6587672cbaa"}, + {file = "Pillow-5.4.1-py3.6-win32.egg", hash = "sha256:87fe838f9dac0597f05f2605c0700b1926f9390c95df6af45d83141e0c514bd9"}, + {file = "Pillow-5.4.1-py3.7-win-amd64.egg", hash = "sha256:c8939dba1a37960a502b1a030a4465c46dd2c2bca7adf05fa3af6bea594e720e"}, + {file = "Pillow-5.4.1-py3.7-win32.egg", hash = "sha256:5337ac3280312aa065ed0a8ec1e4b6142e9f15c31baed36b5cd964745853243f"}, + {file = "Pillow-5.4.1.tar.gz", hash = "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f"}, + {file = "Pillow-5.4.1.win-amd64-py2.7.exe", hash = "sha256:e1555d4fda1db8005de72acf2ded1af660febad09b4708430091159e8ae1963e"}, + {file = "Pillow-5.4.1.win-amd64-py3.4.exe", hash = "sha256:52e2e56fc3706d8791761a157115dc8391319720ad60cc32992350fda74b6be2"}, + {file = "Pillow-5.4.1.win-amd64-py3.5.exe", hash = "sha256:ba6ef2bd62671c7fb9cdb3277414e87a5cd38b86721039ada1464f7452ad30b2"}, + {file = "Pillow-5.4.1.win-amd64-py3.6.exe", hash = "sha256:39fbd5d62167197318a0371b2a9c699ce261b6800bb493eadde2ba30d868fe8c"}, + {file = "Pillow-5.4.1.win-amd64-py3.7.exe", hash = "sha256:5ccd97e0f01f42b7e35907272f0f8ad2c3660a482d799a0c564c7d50e83604d4"}, + {file = "Pillow-5.4.1.win32-py2.7.exe", hash = "sha256:db418635ea20528f247203bf131b40636f77c8209a045b89fa3badb89e1fcea0"}, + {file = "Pillow-5.4.1.win32-py3.4.exe", hash = "sha256:ac036b6a6bac7010c58e643d78c234c2f7dc8bb7e591bd8bc3555cf4b1527c28"}, + {file = "Pillow-5.4.1.win32-py3.5.exe", hash = "sha256:f0e3288b92ca5dbb1649bd00e80ef652a72b657dc94989fa9c348253d179054b"}, + {file = "Pillow-5.4.1.win32-py3.6.exe", hash = "sha256:4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d"}, + {file = "Pillow-5.4.1.win32-py3.7.exe", hash = "sha256:75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522"}, +] +psutil = [ + {file = "psutil-5.7.2-cp27-none-win32.whl", hash = "sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2"}, + {file = "psutil-5.7.2-cp27-none-win_amd64.whl", hash = "sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195"}, + {file = "psutil-5.7.2-cp35-cp35m-win32.whl", hash = "sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c"}, + {file = "psutil-5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6"}, + {file = "psutil-5.7.2-cp36-cp36m-win32.whl", hash = "sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf"}, + {file = "psutil-5.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8"}, + {file = "psutil-5.7.2-cp37-cp37m-win32.whl", hash = "sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"}, + {file = "psutil-5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1"}, + {file = "psutil-5.7.2-cp38-cp38-win32.whl", hash = "sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498"}, + {file = "psutil-5.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f"}, + {file = "psutil-5.7.2.tar.gz", hash = "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb"}, +] +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.5.3-py3-none-any.whl", hash = "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"}, + {file = "pylint-2.5.3.tar.gz", hash = "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc"}, +] +pynpm = [ + {file = "pynpm-0.1.2-py2.py3-none-any.whl", hash = "sha256:3f03fbf667549f8b8b7e0419eef88d1b21833ce288f96de66fbb761b9f4c4061"}, + {file = "pynpm-0.1.2.tar.gz", hash = "sha256:8a6d3f9423760cf3c142db3bf9bda5a075e8a91837e7d2e2b0a3de5be5e26da2"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyserial = [ + {file = "pyserial-3.4-py2.py3-none-any.whl", hash = "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"}, + {file = "pyserial-3.4.tar.gz", hash = "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +rope = [ + {file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"}, + {file = "rope-0.14.0-py3-none-any.whl", hash = "sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"}, + {file = "rope-0.14.0.tar.gz", hash = "sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf"}, +] +"rpi.gpio" = [ + {file = "RPi.GPIO-0.7.0.tar.gz", hash = "sha256:7424bc6c205466764f30f666c18187a0824077daf20b295c42f08aea2cb87d3f"}, +] +sangaboard = [ + {file = "sangaboard-0.2.3-py3-none-any.whl", hash = "sha256:3791159d57a749571f89acdce70aa9c1de82afef71f5186d5e2845c7be44698c"}, + {file = "sangaboard-0.2.3.tar.gz", hash = "sha256:ed0c5a9864f5901d11bed0ed2387da3b5555f75fef72a9f35997304447f9b08c"}, +] +scipy = [ + {file = "scipy-1.4.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c5cac0c0387272ee0e789e94a570ac51deb01c796b37fb2aad1fb13f85e2f97d"}, + {file = "scipy-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a144811318853a23d32a07bc7fd5561ff0cac5da643d96ed94a4ffe967d89672"}, + {file = "scipy-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71eb180f22c49066f25d6df16f8709f215723317cc951d99e54dc88020ea57be"}, + {file = "scipy-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:770254a280d741dd3436919d47e35712fb081a6ff8bafc0f319382b954b77802"}, + {file = "scipy-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:a1aae70d52d0b074d8121333bc807a485f9f1e6a69742010b33780df2e60cfe0"}, + {file = "scipy-1.4.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:bb517872058a1f087c4528e7429b4a44533a902644987e7b2fe35ecc223bc408"}, + {file = "scipy-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:dba8306f6da99e37ea08c08fef6e274b5bf8567bb094d1dbe86a20e532aca088"}, + {file = "scipy-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:386086e2972ed2db17cebf88610aab7d7f6e2c0ca30042dc9a89cf18dcc363fa"}, + {file = "scipy-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:8d3bc3993b8e4be7eade6dcc6fd59a412d96d3a33fa42b0fa45dc9e24495ede9"}, + {file = "scipy-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dc60bb302f48acf6da8ca4444cfa17d52c63c5415302a9ee77b3b21618090521"}, + {file = "scipy-1.4.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:787cc50cab3020a865640aba3485e9fbd161d4d3b0d03a967df1a2881320512d"}, + {file = "scipy-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0902a620a381f101e184a958459b36d3ee50f5effd186db76e131cbefcbb96f7"}, + {file = "scipy-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:00af72998a46c25bdb5824d2b729e7dabec0c765f9deb0b504f928591f5ff9d4"}, + {file = "scipy-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:9508a7c628a165c2c835f2497837bf6ac80eb25291055f56c129df3c943cbaf8"}, + {file = "scipy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a2d6df9eb074af7f08866598e4ef068a2b310d98f87dc23bd1b90ec7bdcec802"}, + {file = "scipy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3092857f36b690a321a662fe5496cb816a7f4eecd875e1d36793d92d3f884073"}, + {file = "scipy-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8a07760d5c7f3a92e440ad3aedcc98891e915ce857664282ae3c0220f3301eb6"}, + {file = "scipy-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1e3190466d669d658233e8a583b854f6386dd62d655539b77b3fa25bfb2abb70"}, + {file = "scipy-1.4.1-cp38-cp38-win32.whl", hash = "sha256:cc971a82ea1170e677443108703a2ec9ff0f70752258d0e9f5433d00dda01f59"}, + {file = "scipy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:2cce3f9847a1a51019e8c5b47620da93950e58ebc611f13e0d11f4980ca5fecb"}, + {file = "scipy-1.4.1.tar.gz", hash = "sha256:dee1bbf3a6c8f73b6b218cb28eed8dd13347ea2f87d572ce19b289d6fd3fbc59"}, +] +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.1.2-py3-none-any.whl", hash = "sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00"}, + {file = "Sphinx-3.1.2.tar.gz", hash = "sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"}, +] +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-httpdomain = [ + {file = "sphinxcontrib-httpdomain-1.7.0.tar.gz", hash = "sha256:ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"}, + {file = "sphinxcontrib_httpdomain-1.7.0-py2.py3-none-any.whl", hash = "sha256:1fb5375007d70bf180cdd1c79e741082be7aa2d37ba99efe561e1c2e3f38191e"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.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"}, +] +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.0-py3-none-any.whl", hash = "sha256:8c448ad37ed074ce8811c9eb2765c01714a93f977a1c04fc39fbf6f516b0566f"}, + {file = "zeroconf-0.28.0.tar.gz", hash = "sha256:881da2ed3d7c8e0ab59fb1cc8b02b53134351941c4d8d3f3553a96700f257a03"}, +] diff --git a/pyproject.toml b/pyproject.toml index 4b16704a..e2542d1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.poetry] -name = "openflexure_microscope" -version = "1.1.2" +name = "openflexure-microscope-server" +version = "2.4.1" description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope." authors = [ @@ -22,19 +22,41 @@ keywords = ['raspberry pi', 'arduino', 'microscope'] license = "GPL-3.0" -[tool.poetry.dependencies] -python = "^3.5" -Flask = "^1.0" -pyyaml = "^3.13" -numpy = "1.16.4" # Exact version until we can guarantee a wheel -Pillow = "^5.4" -openflexure-stage = "^0.2.1" -flask-cors = "^3.0" -scipy = "1.3.0" # Exact version until we can guarantee a wheel +exclude = ["**/node_modules/"] -picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading" } # Lens shading requires >1.14 or RWB fork, lens-shading branch. -"RPi.GPIO" = { version = "^0.6.5", optional = true } +packages = [ + { include = "openflexure_microscope" }, +] + +[tool.poetry.dependencies] +python = "^3.6" +Flask = "^1.0" +numpy = "1.18.2" +Pillow = "^5.4" +scipy = "1.4.1" # Exact version so we can guarantee a wheel +"RPi.GPIO" = { version = "^0.7.0", optional = true } +picamera = {url = "https://github.com/rwb27/picamera/releases/download/v1.13.2b0/picamera-1.13.2b0-py3-none-any.whl" } +python-dateutil = "^2.8" +psutil = "^5.6.7" # Autostorage extension +opencv-python-headless = [ + {version = "4.1.0.25", python = "~3.7"}, # PiWheels build for RPi running Py37 + {version = "4.2.0.34", python = "^3.8"} # Latest for Py38 systems +] +pynpm = "^0.1.2" +sangaboard = "^0.2" +expiringdict = "^1.2.1" +camera-stage-mapping = "^0.1.2" +labthings = "0.8.0" + +[tool.poetry.extras] +rpi = ["RPi.GPIO"] + +[tool.poetry.scripts] +build_static = 'openflexure_microscope.install:build' [tool.poetry.dev-dependencies] +pynpm = "^0.1.2" sphinxcontrib-httpdomain = "^1.7" rope = "^0.14.0" +pylint = "^2.3" +black = {version = "^18.3-alpha.0",allow-prereleases = true} diff --git a/readthedocs.yaml b/readthedocs.yaml index b05eb087..ab466fe5 100644 --- a/readthedocs.yaml +++ b/readthedocs.yaml @@ -5,5 +5,5 @@ sphinx: configuration: docs/source/conf.py python: - version: 3.5 + version: 3.7 pip_install: true \ No newline at end of file diff --git a/tests/api_client.py b/tests/api_client.py index 4201b6d3..e2fd1090 100644 --- a/tests/api_client.py +++ b/tests/api_client.py @@ -30,63 +30,61 @@ class APIconnection: return r.json() def set_overlay(self, message="", size=50): - json = { - "text": message, - "size": size - } - return self.post('/camera/overlay', json=json) + json = {"text": message, "size": size} + return self.post("/camera/overlay", json=json) def get_overlay(self): - return self.get('/camera/overlay') + return self.get("/camera/overlay") def get_config(self): - return self.get('/config') + return self.get("/config") def set_config(self, config_dict): - return self.post('/config', json=config_dict) + return self.post("/config", json=config_dict) def get_state(self): - return self.get('/state') + return self.get("/state") def start_preview(self): - return self.post('/camera/preview/start') + return self.post("/camera/preview/start") def stop_preview(self): - return self.post('/camera/preview/stop') + return self.post("/camera/preview/stop") def move_by(self, x=0, y=0, z=0): - json = { - "x": x, - "y": y, - "z": z - } - return self.post('/stage/position', json=json) + json = {"x": x, "y": y, "z": z} + return self.post("/stage/position", json=json) def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None): - json = { - "keep_on_disk": keep_on_disk, - "use_video_port": use_video_port - } + json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port} if resize: - json['size'] = {'width': resize[0], 'height': resize[1]} + json["size"] = {"width": resize[0], "height": resize[1]} - return self.post('/camera/capture', json=json) + return self.post("/camera/capture", json=json) def get_capture(self, capture_id): - uri_route = '/camera/capture/{}/download'.format(capture_id) + uri_route = "/camera/capture/{}/download".format(capture_id) r = self.get(uri_route, json=False) img = Image.open(BytesIO(r.content)) array = np.asarray(img, dtype=np.int32) return array def del_capture(self, capture_id): - uri_route = '/camera/capture/{}'.format(capture_id) + uri_route = "/camera/capture/{}".format(capture_id) return self.delete(uri_route) - def capture(self, use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=None): - p = self.new_capture(use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize) - capture_id = p['metadata']['id'] + def capture( + self, + use_video_port=True, + keep_on_disk=False, + delete_after_use=True, + resize=None, + ): + p = self.new_capture( + use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize + ) + capture_id = p["metadata"]["id"] img_array = self.get_capture(capture_id) if delete_after_use: @@ -95,10 +93,8 @@ class APIconnection: return img_array def set_zoom(self, zoom_value=1.0): - json = { - "zoom_value": zoom_value, - } - return self.post('/camera/zoom', json=json) + json = {"zoom_value": zoom_value} + return self.post("/camera/zoom", json=json) def get_zoom(self): - return self.get('/camera/zoom') + return self.get("/camera/zoom") diff --git a/tests/test_api.py b/tests/test_api.py index 60a2142b..83e53d08 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,27 +6,23 @@ import unittest import logging import sys + logging.basicConfig(stream=sys.stderr, level=logging.INFO) class TestCapture(unittest.TestCase): - def test_capture_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() - - expected_keys = [ - 'image_resolution', - 'stream_resolution', - 'numpy_resolution', - ] + + expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"] for key in expected_keys: self.assertTrue(key in config) def test_capture_videoport(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") - resolution = connection.get_config()['stream_resolution'] + resolution = connection.get_config()["stream_resolution"] for resize in [None, (640, 480)]: @@ -34,16 +30,17 @@ class TestCapture(unittest.TestCase): resolution = resize capture_array = connection.capture( - use_video_port=True, - keep_on_disk=False, + use_video_port=True, + keep_on_disk=False, delete_after_use=True, - resize=resize) + resize=resize, + ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) def test_capture_full(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") - resolution = connection.get_config()['image_resolution'] + resolution = connection.get_config()["image_resolution"] for resize in [None, (640, 480)]: @@ -51,23 +48,21 @@ class TestCapture(unittest.TestCase): resolution = resize capture_array = connection.capture( - use_video_port=False, - keep_on_disk=False, + use_video_port=False, + keep_on_disk=False, delete_after_use=True, - resize=resize) + resize=resize, + ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) class TestStage(unittest.TestCase): - def test_stage_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() - - expected_keys = [ - 'backlash', - ] + + expected_keys = ["backlash"] for key in expected_keys: self.assertTrue(key in config) @@ -76,14 +71,12 @@ class TestStage(unittest.TestCase): connection = APIconnection(host="localhost", port=5000, api_ver="v1") state = connection.get_state() - self.assertTrue('stage' in state) - - expected_keys = [ - 'position', - ] + self.assertTrue("stage" in state) + + expected_keys = ["position"] for key in expected_keys: - self.assertTrue(key in state['stage']) + self.assertTrue(key in state["stage"]) def test_stage_movement(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") @@ -91,16 +84,16 @@ class TestStage(unittest.TestCase): move_distance = 500 for axis in range(3): for direction in [1, -1]: - pos_i_dict = connection.get_state()['stage']['position'] - pos_i = [pos_i_dict['x'], pos_i_dict['y'], pos_i_dict['z']] + pos_i_dict = connection.get_state()["stage"]["position"] + pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]] move = [0, 0, 0] - move[axis] = move_distance*direction + move[axis] = move_distance * direction connection.move_by(*move) - pos_f_dict = connection.get_state()['stage']['position'] - pos_f = [pos_f_dict['x'], pos_f_dict['y'], pos_f_dict['z']] + pos_f_dict = connection.get_state()["stage"]["position"] + pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]] diff = np.subtract(pos_f, pos_i) logging.debug("{} > {}".format(pos_i, pos_f)) @@ -108,7 +101,7 @@ class TestStage(unittest.TestCase): self.assertTrue(np.array_equal(diff, move)) -if __name__ == '__main__': +if __name__ == "__main__": suites = [ unittest.TestLoader().loadTestsFromTestCase(TestCapture), diff --git a/tests/test_camera.py b/tests/test_camera.py index 0188fa2f..6369a651 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -12,11 +12,11 @@ import unittest import logging import sys + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) class TestCaptureMethods(unittest.TestCase): - def test_still_capture(self): """Tests capturing still images to a BytesIO stream.""" global camera @@ -28,13 +28,11 @@ class TestCaptureMethods(unittest.TestCase): camera.wait_for_camera() # Capture to a context (auto-deletes files when done) - with camera.new_image(write_to_file=False) as output: + with camera.new_image() as output: camera.capture( - output, - use_video_port=use_video_port, - resize=resize - ) + output, use_video_port=use_video_port, resize=resize + ) # Ensure file deletion fails and returns False self.assertFalse(output.delete_file()) @@ -43,14 +41,8 @@ class TestCaptureMethods(unittest.TestCase): # BEFORE DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string - self.assertTrue(isinstance( - output.data, - io.IOBase - )) - self.assertTrue(isinstance( - output.binary, - (bytes, bytearray) - )) + self.assertTrue(isinstance(output.data, io.IOBase)) + self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Save capture to file output.save_file() @@ -65,10 +57,7 @@ class TestCaptureMethods(unittest.TestCase): # AFTER DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string self.assertTrue(isinstance(output.data, io.IOBase)) - self.assertTrue(isinstance( - output.binary, - (bytes, bytearray) - )) + self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Create a PIL image from stream image = Image.open(output.data) @@ -100,9 +89,10 @@ class TestCaptureMethods(unittest.TestCase): # Capture output = camera.capture( - camera.new_image(write_to_file=True), + camera.new_image().file, use_video_port=use_video_port, - resize=resize) + resize=resize, + ) # Check file got saved self.assertTrue(os.path.isfile(output.file)) @@ -122,7 +112,6 @@ class TestCaptureMethods(unittest.TestCase): class TestUnencodedMethods(unittest.TestCase): - def test_yuv_array(self): """Tests capturing unencoded YUV data to a Numpy array.""" global camera @@ -136,9 +125,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array - yuv = camera.yuv( - use_video_port=use_video_port, - resize=resize) + yuv = camera.yuv(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(yuv, np.ndarray)) @@ -168,9 +155,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array - rgb = camera.array( - use_video_port=use_video_port, - resize=resize) + rgb = camera.array(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(rgb, np.ndarray)) @@ -189,41 +174,33 @@ class TestUnencodedMethods(unittest.TestCase): class TestRecordMethods(unittest.TestCase): - def test_video_record(self): """Tests recording videos to BytesIO stream, and to file on disk.""" global camera - for write_to_file in [True, False, None]: + # Wait for camera + camera.wait_for_camera() - logging.debug("\nWRITE TO FILE: {}".format(write_to_file)) + with camera.new_video() as output: - # Wait for camera - camera.wait_for_camera() + # Start recording + camera.start_recording(output) - with camera.new_video( - write_to_file=write_to_file - ) as output: + # Record for 2 seconds + time.sleep(2) + # Stop recording + camera.stop_recording() - # Start recording - camera.start_recording(output) + # Check stream + self.assertTrue(isinstance(output.data, io.IOBase)) + self.assertTrue(isinstance(output.binary, (bytes, bytearray))) - # Record for 2 seconds - time.sleep(2) - # Stop recording - camera.stop_recording() + # Check file + statinfo = os.stat(output.file) + self.assertTrue(statinfo.st_size > 0) - # Check stream - self.assertTrue(isinstance(output.data, io.IOBase)) - self.assertTrue(isinstance(output.binary, (bytes, bytearray))) - - # Check file - if write_to_file: - statinfo = os.stat(output.file) - self.assertTrue(statinfo.st_size > 0) - - # Log path - temp_path = output.file + # Log path + temp_path = output.file # Check file got deleted on __exit__ self.assertFalse(os.path.isfile(temp_path)) @@ -278,7 +255,7 @@ class TestThreadStarting(unittest.TestCase): self.assertIsInstance(camera.stream, io.IOBase) -if __name__ == '__main__': +if __name__ == "__main__": with PiCameraStreamer() as camera: suites = [ diff --git a/tests/test_plugins.py b/tests/test_plugins.py index ebf9c449..a6c9895a 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -8,39 +8,37 @@ import atexit import unittest import logging, sys + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) class TestPluginMethods(unittest.TestCase): - def test_plugin_load(self): - plugin_arr = microscope.plugin.plugins + plugin_arr = microscope.plugins.plugins plugin_names = [plugin[0] for plugin in plugin_arr] - self.assertTrue('testing' in plugin_names) + self.assertTrue("testing" in plugin_names) def test_camera_access(self): - identify = microscope.plugin.testing.identify() + identify = microscope.plugins.testing.identify() self.assertTrue(identify[0] is microscope.camera) def test_stage_access(self): - identify = microscope.plugin.testing.identify() + identify = microscope.plugins.testing.identify() self.assertTrue(identify[1] is microscope.stage) -if __name__ == '__main__': +if __name__ == "__main__": - with Microscope() as microscope: + with Microscope() as microscope: - microscope.attach(PiCameraStreamer(), OpenFlexureStage()) + microscope.attach(PiCameraStreamer(), OpenFlexureStage()) - microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin") + microscope.plugins.attach("openflexure_microscope.plugins.testing:Plugin") - suites = [ - unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods), - ] + suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)] - alltests = unittest.TestSuite(suites) + alltests = unittest.TestSuite(suites) - result = unittest.TextTestRunner(verbosity=2).run(alltests) + result = unittest.TextTestRunner(verbosity=2).run(alltests) diff --git a/tests/test_stage.py b/tests/test_stage.py index 90b147be..b211bc76 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -12,7 +12,6 @@ logging.basicConfig(stream=sys.stderr, level=logging.INFO) class TestMicroscope(unittest.TestCase): - def test_movement(self): move_distance = 500 for axis in range(3): @@ -20,9 +19,11 @@ class TestMicroscope(unittest.TestCase): pos_i = stage.position logging.debug(pos_i) - logging.info("Moving axis {} by {}".format(axis, move_distance*direction)) + logging.info( + "Moving axis {} by {}".format(axis, move_distance * direction) + ) move = [0, 0, 0] - move[axis] = move_distance*direction + move[axis] = move_distance * direction stage.move_rel(move) @@ -33,12 +34,10 @@ class TestMicroscope(unittest.TestCase): self.assertTrue(np.array_equal(diff, move)) -if __name__ == '__main__': +if __name__ == "__main__": with OpenFlexureStage("/dev/ttyUSB0") as stage: - suites = [ - unittest.TestLoader().loadTestsFromTestCase(TestMicroscope), - ] + suites = [unittest.TestLoader().loadTestsFromTestCase(TestMicroscope)] alltests = unittest.TestSuite(suites)