Repository: rambo/python-scpi (language: python)

Issue:
TypeError: object Lock can't be used in 'await' expression
```
    with (await self.lock):
TypeError: object Lock can't be used in 'await' expression
```

I get this with python 3.9 while trying to use the TCPTransport
Is there any type of incompatibility with py 3.9 ?


Edit: Relevant:
https://stackoverflow.com/questions/65111601/what-is-the-difference-between-async-with-lock-and-with-await-lock
"The second form with await lock is deprecated since Python 3.7 and is removed in Python 3.9."

SCPIProtocol potential infinite recursion
In the command and ask methods there is a potential for infinite recursion/loop
```python
except (asyncio.TimeoutError, asyncio.CancelledError) as err:
    # check for the actual error if available
    await self.check_error(command)
    if abort_on_timeout:
        self.abort_command()
    # re-raise the timeout if no other error found
    raise err
# other errors are allowed to bubble-up as-is
```
The `self.check_error(command)` calls `self.get_error()` which calls `self.ask`.

If `transport.get_response` times out or is cancelled (`asyncio.TimeoutError` or `asyncio.CancelledError`), this will result in `self.check_error`, and therefore `self.ask`, being called again and again. I discovered this because my transport layer was trying to read too many bytes and always timed out (think binary data read from a spectrum analyzer or network analyzer, or trying to clear the input buffers of excess data).

To create the issue, try adding the following to the transport layer's `get_response`.
```python
with await self.lock:
    data = await self.reader.readline()
    await self.reader.read()
    return data
```
or in my case `get_response` takes `nbytes` as an argument and it will timeout if `nbytes` is larger then bytes in the buffer.
```python
with await self.lock:
    data = await self.reader.read(n=nbytes)
    return data
```

My quick solution and not very pretty was to set and check a flag if errors were already being checked.
```python
except (asyncio.TimeoutError, asyncio.CancelledError) as err:
    # check for the actual error if available
     if not self.checkingerror:
        self.checkingerror = True
        await self.check_error(cmd)
    self.checkingerror = False
    if abort_on_timeout:
        self.abort_command()
    # re-raise the timeout if no other error found
    raise err
# other errors are allowed to bubble-up as-is
```



Discussion / hints:
I wonder if `async with self.lock` just works on older python versions as well. I was very new to asyncio back then I made this so there's a bunch of other not-great stuff there too, the next time I need to actually do something with SCPI I will probably end up doing some heavy duty refactoring/rewriting.
Also it seems TCPTransport wasn't even done by me :)
> 
> 
> Also it seems TCPTransport wasn't even done by me :)

Right. I needed to use TCPTransport , but it's not in the Pypi package. So please, push it to Pypi if you get around fixing it :-)
> 
> 
> I wonder if `async with self.lock` just works on older python versions as well. I was very new to asyncio back then I made this so there's a bunch of other not-great stuff there too, the next time I need to actually do something with SCPI I will probably end up doing some heavy duty refactoring/rewriting.

I've done a quick, non-exhaustive check and `async with self.lock` seems to work on python 3.7.9
(I only changed it in SCPIProtocol.ask())


Fix patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
new file mode 100644
index 0000000..95fe327
--- /dev/null
+++ b/.bumpversion.cfg
@@ -0,0 +1,16 @@
+[bumpversion]
+current_version = 2.2.0
+commit = False
+tag = False
+
+[bumpversion:file:pyproject.toml]
+search = version = "{current_version}"
+replace = version = "{new_version}"
+
+[bumpversion:file:src/scpi/__init__.py]
+search = __version__ = "{current_version}"
+replace = __version__ = "{new_version}"
+
+[bumpversion:file:tests/test_scpi.py]
+search = __version__ == "{current_version}"
+replace = __version__ == "{new_version}"
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..747ceb4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,53 @@
+# General good ideas to ignore
+.idea
+.git
+.cache
+.venv
+
+# docker build files (so just changing them does not invalidate *all* caches)
+.dockerignore
+Dockerfile
+
+# Byte-compiled / optimized / DLL files
+**/__pycache__/
+**/*.py[cod]
+**/*$py.class
+
+# Distribution / packaging
+**/.Python
+**/build/
+**/develop-eggs/
+**/dist/
+**/downloads/
+**/eggs/
+**/.eggs/
+**/lib/
+**/lib64/
+**/parts/
+**/sdist/
+**/var/
+**/wheels/
+**/pip-wheel-metadata/
+**/share/python-wheels/
+**/*.egg-info/
+**/.installed.cfg
+**/*.egg
+
+# Unit test / coverage reports
+**/htmlcov/
+**/.tox/
+**/.nox/
+**/.coverage
+**/.coverage.*
+**/.cache
+**/nosetests.xml
+**/coverage.xml
+**/*.cover
+**/*.py,cover
+**/.hypothesis/
+**/.pytest_cache/
+
+# mypy
+**/.mypy_cache/
+**/.dmypy.json
+**/dmypy.json
diff --git a/.flake8 b/.flake8
index 5f32207..6deafc2 100644
--- a/.flake8
+++ b/.flake8
@@ -1,3 +1,2 @@
 [flake8]
 max-line-length = 120
-
diff --git a/.gitignore b/.gitignore
index 5cabfba..38c502c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,41 +1,135 @@
+# IDE settings
 .idea
-MANIFEST
-.DS_Store
-._*
+
+# ci artefacts
+pytest*.xml
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
 *.py[cod]
+*$py.class
 
 # C extensions
 *.so
 
-# Packages
-*.egg
-*.egg-info
-dist
-build
-eggs
-parts
-bin
-var
-sdist
-develop-eggs
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
 .installed.cfg
-lib
-lib64
-__pycache__
+*.egg
+MANIFEST
+
+# 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/
+.nox/
 .coverage
-.tox
+.coverage.*
+.cache
 nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
 
 # Translations
 *.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
 
-# Mr Developer
-.mr.developer.cfg
-.project
-.pydevproject
-/venv*/
+# Pyre type checker
+.pyre/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..b713fa3
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,62 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+default_language_version:
+    python: python3.7
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v2.4.0
+    hooks:
+    -   id: no-commit-to-branch
+    -   id: check-executables-have-shebangs
+    -   id: check-ast
+    -   id: check-toml
+    -   id: trailing-whitespace
+    -   id: end-of-file-fixer
+    -   id: check-yaml
+    -   id: check-added-large-files
+    -   id: check-case-conflict
+    -   id: check-json
+    -   id: check-merge-conflict
+    -   id: check-symlinks
+    -   id: pretty-format-json
+        args:
+        - --autofix
+-   repo: https://github.com/psf/black
+    rev: 20.8b1
+    hooks:
+    -   id: black
+        language_version: python3.7
+#-   repo: https://github.com/pre-commit/mirrors-mypy
+#    rev: v0.800
+#    hooks:
+#    -   id: mypy
+#        language: system
+#        args: [--strict] #, --ignore-missing-imports]
+#-   repo: https://github.com/pre-commit/mirrors-pylint
+#    rev: v2.4.4
+#    hooks:
+#    -   id: pylint
+#        language: system
+-   repo: https://github.com/Lucas-C/pre-commit-hooks
+    rev: v1.1.7
+    hooks:
+    -   id: forbid-crlf
+    -   id: remove-crlf
+    -   id: forbid-tabs
+    -   id: remove-tabs
+-   repo: https://github.com/PyCQA/bandit
+    rev: 1.6.2
+    hooks:
+    - id: bandit
+      args: ["--skip=B101"]
+-   repo: git://github.com/Lucas-C/pre-commit-hooks-markup
+    rev: v1.0.0
+    hooks:
+    -   id: rst-linter
+-   repo: https://github.com/Yelp/detect-secrets
+    rev: v1.0.3
+    hooks:
+    -   id: detect-secrets
+        language: system
+        exclude: "poetry.lock"
+        # args: ['--baseline', '.secrets.baseline']
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..bf8ed68
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,146 @@
+# syntax=docker/dockerfile:1.1.7-experimental
+#############################################
+# Tox testsuite for multiple python version #
+#############################################
+FROM advian/tox-base:alpine as tox
+ARG PYTHON_VERSIONS="3.8.7 3.9.1 3.7.9 3.6.12"
+ARG POETRY_VERSION="1.1.4"
+RUN for pyver in $PYTHON_VERSIONS; do pyenv install -s $pyver; done \
+    && pyenv global $PYTHON_VERSIONS \
+    && poetry self update $POETRY_VERSION || pip install -U poetry==$POETRY_VERSION \
+    && python -m pip install -U tox \
+    && apk add --no-cache \
+        git \
+    && poetry install \
+    && docker/pre_commit_init.sh \
+    && true
+
+######################
+# Base builder image #
+######################
+FROM python:3.7-alpine as builder_base
+
+ENV \
+  # locale
+  LC_ALL=C.UTF-8 \
+  # python:
+  PYTHONFAULTHANDLER=1 \
+  PYTHONUNBUFFERED=1 \
+  PYTHONHASHSEED=random \
+  # pip:
+  PIP_NO_CACHE_DIR=off \
+  PIP_DISABLE_PIP_VERSION_CHECK=on \
+  PIP_DEFAULT_TIMEOUT=100 \
+  # poetry:
+  POETRY_VERSION=1.1.4
+
+
+RUN apk add --no-cache \
+        curl \
+        git \
+        bash \
+        build-base \
+        libffi-dev \
+        linux-headers \
+        openssl \
+        openssl-dev \
+        zeromq \
+        tini \
+        openssh-client \
+        cargo \
+    # githublab ssh
+    && mkdir -p -m 0700 ~/.ssh && ssh-keyscan gitlab.com github.com | sort > ~/.ssh/known_hosts \
+    # Install poetry package manager their way (pypi package sometimes has issues)
+    && curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 \
+    && echo 'source $HOME/.poetry/env' >>/root/.profile \
+    && source $HOME/.poetry/env \
+    # We're in a container, do not create useless virtualenvs
+    && poetry config virtualenvs.create false \
+    && true
+
+SHELL ["/bin/bash", "-lc"]
+
+
+# Copy only requirements, to cache them in docker layer:
+WORKDIR /pysetup
+COPY ./poetry.lock ./pyproject.toml /pysetup/
+# Install basic requirements (utilizing an internal docker wheelhouse if available)
+RUN --mount=type=ssh pip3 install wheel \
+    && poetry export -f requirements.txt --without-hashes -o /tmp/requirements.txt \
+    && pip3 wheel --wheel-dir=/tmp/wheelhouse --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 -r /tmp/requirements.txt \
+    && pip3 install --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 --find-links=/tmp/wheelhouse/ /tmp/wheelhouse/*.whl \
+    && true
+
+
+####################################
+# Base stage for production builds #
+####################################
+FROM builder_base as production_build
+# Copy entrypoint script
+COPY ./docker/entrypoint.sh /docker-entrypoint.sh
+# Only files needed by production setup
+COPY ./poetry.lock ./pyproject.toml ./README.rst ./src /app/
+WORKDIR /app
+# Build the wheel package with poetry and add it to the wheelhouse
+RUN --mount=type=ssh poetry build -f wheel --no-interaction --no-ansi \
+    && cp dist/*.whl /tmp/wheelhouse \
+    && chmod a+x /docker-entrypoint.sh \
+    && true
+
+
+#########################
+# Main production build #
+#########################
+FROM python:3.7-alpine as production
+COPY --from=production_build /tmp/wheelhouse /tmp/wheelhouse
+COPY --from=production_build /docker-entrypoint.sh /docker-entrypoint.sh
+WORKDIR /app
+# Install system level deps for running the package (not devel versions for building wheels)
+# and install the wheels we built in the previous step. generate default config
+RUN --mount=type=ssh apk add --no-cache \
+        bash \
+        tini \
+    && chmod a+x /docker-entrypoint.sh \
+    && pip3 install --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 --find-links=/tmp/wheelhouse/ /tmp/wheelhouse/scpi-*.whl \
+    && rm -rf /tmp/wheelhouse/ \
+    # Do whatever else you need to
+    && true
+ENTRYPOINT ["/sbin/tini", "--", "/docker-entrypoint.sh"]
+
+
+#####################################
+# Base stage for development builds #
+#####################################
+FROM builder_base as devel_build
+# Install deps
+WORKDIR /pysetup
+RUN --mount=type=ssh poetry install --no-interaction --no-ansi \
+    && true
+
+
+#0############
+# Run tests #
+#############
+FROM devel_build as test
+COPY . /app
+WORKDIR /app
+ENTRYPOINT ["/sbin/tini", "--", "docker/entrypoint-test.sh"]
+# Re run install to get the service itself installed
+RUN --mount=type=ssh poetry install --no-interaction --no-ansi \
+    && docker/pre_commit_init.sh \
+    && true
+
+
+###########
+# Hacking #
+###########
+FROM devel_build as devel_shell
+# Copy everything to the image
+COPY . /app
+WORKDIR /app
+RUN apk add --no-cache zsh \
+    && sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \
+    && echo "source /root/.profile" >>/root/.zshrc \
+    && pip3 install git-up \
+    && true
+ENTRYPOINT ["/bin/zsh", "-l"]
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 922846e..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,3 +0,0 @@
-include README.md
-recursive-include scpi *.py
-recursive-exclude scpi ._*
diff --git a/README b/README
index 42061c0..92cacd2 120000
--- a/README
+++ b/README
@@ -1,1 +1,1 @@
-README.md
\ No newline at end of file
+README.rst
\ No newline at end of file
diff --git a/README.md b/README.rst
similarity index 73%
rename from README.md
rename to README.rst
index 92d9ccc..c460cc6 100644
--- a/README.md
+++ b/README.rst
@@ -1,7 +1,8 @@
-python-scpi
-===========
+====
+scpi
+====
 
-**New [asyncio][asyncio] version. Only for Python 3.5 and above**
+New asyncio_ version. Only for Python 3.6 and above
 
 Since all the other wrappers either require VISA binary or are not generic (and do not implement the device I need)
 
@@ -9,10 +10,15 @@ Basic idea here is to make transport-independent command sender/parser and a dev
 
 A device specific implementation can then add the device-specific commands.
 
-Pro tip for thos wishing to work on the code <http://guide.python-distribute.org/pip.html#installing-from-a-vcs>
+Pro tip for thos wishing to work on the code https://python-poetry.org/
+
+.. _asyncio: https://docs.python.org/3/library/asyncio.html
+
 
 ## Usage
 
+Install the package to your virtualenv with poetry or from pip
+
   - Instatiate a transport (for GPIB you will need `GPIBDeviceTransport` to be able to use the device helper class)
   - Instatiate `SCPIProtocol` with the transport (optional, see below)
   - Instantiate `SCPIDevice` with the protocol (or as a shorthand: with the transport directly)
@@ -21,16 +27,15 @@ Pro tip for thos wishing to work on the code <http://guide.python-distribute.org
 Or if you're just playing around in the REPL use `AIOWrapper` to hide the eventloop handling
 for traditional non-concurrent approach.
 
-See <https://github.com/rambo/python-scpi/tree/master/examples> for more.
+See the examples directory for more.
 
-## TODO
+TODO
+----
 
-### Check Carrier-Detect for RS232 transport
+Check Carrier-Detect for RS232 transport
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 in the RS232 transport check getCD to make sure the device is present before doing anything.
 CTS can also be checked even if hw flow control is not in use.
 
 Basically wait for it for X seconds and abort if not found
-
-
-[asyncio]: https://docs.python.org/3/library/asyncio.html
\ No newline at end of file
diff --git a/autopep8_and_friends.sh b/autopep8_and_friends.sh
deleted file mode 100755
index 8b4ceba..0000000
--- a/autopep8_and_friends.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-PYLINTRC=`dirname $0`/.pylintrc
-FLAKE8RC=`dirname $0`/.flake8
-find . -name '._*' | xargs rm
-for f in $(find . -name '*.py' -and -not -path '*/venv/*' -and -not -path '*/build/*')
-do
-    echo "================="
-    echo $f
-    autopep8 -ri --max-line-length=10000 $f
-    flake8 --config=$FLAKE8RC $f
-    isort -rc $f
-    pylint --rcfile=$PYLINTRC $f
-done
-
diff --git a/devel_requirements.txt b/devel_requirements.txt
deleted file mode 100644
index 1098299..0000000
--- a/devel_requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
--r requirements.txt
-autopep8==1.3.5
-flake8==3.5.0
-isort==4.3.4
-pep8==1.7.1
-pep8-naming==0.7.0
-pylint==1.9.2
-pylint-common==0.2.5
-pylint-plugin-utils==0.3
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100755
index 0000000..1a82959
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,8 @@
+#!/bin/bash -l
+set -e
+if [ "$#" -eq 0 ]; then
+  # TODO: Put your actual program start here
+  exec true
+else
+  exec "$@"
+fi
diff --git a/docker/pre_commit_init.sh b/docker/pre_commit_init.sh
new file mode 100755
index 0000000..d02d40d
--- /dev/null
+++ b/docker/pre_commit_init.sh
@@ -0,0 +1,10 @@
+#!/bin/bash -l
+if [ ! -d .git ]
+then
+  git init
+  git checkout -b precommit_init
+  git add .
+fi
+set -e
+poetry run pre-commit install
+poetry run pre-commit run --all-files
diff --git a/examples/hp6632b_serial.py b/examples/hp6632b_serial.py
index 504c841..ae398bc 100755
--- a/examples/hp6632b_serial.py
+++ b/examples/hp6632b_serial.py
@@ -7,12 +7,12 @@
 from scpi.devices import hp6632b
 from scpi.wrapper import AIOWrapper
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     if len(sys.argv) < 2:
         print("run with python -i hp6632b.py /dev/ttyUSB0")
         sys.exit(1)
     # Then put to interactive mode
-    os.environ['PYTHONINSPECT'] = '1'
+    os.environ["PYTHONINSPECT"] = "1"
     aiodev = hp6632b.rs232(sys.argv[1], rtscts=True)
     dev = AIOWrapper(aiodev)
 
diff --git a/examples/prologix_usb.py b/examples/prologix_usb.py
index d053671..ca246a1 100755
--- a/examples/prologix_usb.py
+++ b/examples/prologix_usb.py
@@ -8,12 +8,12 @@
 from scpi.transports.gpib import prologix
 from scpi.wrapper import AIOWrapper
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     if len(sys.argv) < 2:
         print("run with python -i hp6632b.py /dev/ttyUSB0")
         sys.exit(1)
     # Then put to interactive mode
-    os.environ['PYTHONINSPECT'] = '1'
+    os.environ["PYTHONINSPECT"] = "1"
     # Get the low-level GPIB transport
     aiogpib = prologix.get(sys.argv[1])
     # And the mapper that handlers asyncio transparently
diff --git a/examples/tdklambda_tcp.py b/examples/tdklambda_tcp.py
index 120fba2..1d3fb9b 100644
--- a/examples/tdklambda_tcp.py
+++ b/examples/tdklambda_tcp.py
@@ -1,8 +1,8 @@
-'''
+"""
 Created on febrary 21 2020.
 
 @author: qmor
-'''
+"""
 
 import atexit
 import os
@@ -11,10 +11,10 @@
 from scpi.devices import TDKLambdaZPlus
 from scpi.wrapper import AIOWrapper
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     # Then put to interactive mode
-    os.environ['PYTHONINSPECT'] = '1'
-    aiodev = TDKLambdaZPlus.tcp('192.168.3.34', 8003)
+    os.environ["PYTHONINSPECT"] = "1"
+    aiodev = TDKLambdaZPlus.tcp("192.168.3.34", 8003)
     dev = AIOWrapper(aiodev)
     atexit.register(dev.quit)
     dev.identify()
@@ -23,4 +23,3 @@
         dev.query_current()
         dev.query_output()
         time.sleep(1)
-    
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..6a95c59
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1046 @@
+[[package]]
+name = "appdirs"
+version = "1.4.4"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "aspy.yaml"
+version = "1.3.0"
+description = "A few extensions to pyyaml."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+pyyaml = "*"
+
+[[package]]
+name = "astroid"
+version = "2.5.1"
+description = "An abstract syntax tree for Python with inference support."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+lazy-object-proxy = ">=1.4.0"
+typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""}
+wrapt = ">=1.11,<1.13"
+
+[[package]]
+name = "async-timeout"
+version = "3.0.1"
+description = "Timeout context manager for asyncio programs"
+category = "main"
+optional = false
+python-versions = ">=3.5.3"
+
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "20.3.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
+
+[[package]]
+name = "bandit"
+version = "1.7.0"
+description = "Security oriented static analyser for python code."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
+GitPython = ">=1.0.1"
+PyYAML = ">=5.3.1"
+six = ">=1.10.0"
+stevedore = ">=1.20.0"
+
+[[package]]
+name = "black"
+version = "20.8b1"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+appdirs = "*"
+click = ">=7.1.2"
+dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.6,<1"
+regex = ">=2020.1.8"
+toml = ">=0.10.1"
+typed-ast = ">=1.4.0"
+typing-extensions = ">=3.7.4"
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
+
+[[package]]
+name = "bump2version"
+version = "1.0.1"
+description = "Version-bump your software with a single command!"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "certifi"
+version = "2020.12.5"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "cfgv"
+version = "3.0.0"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "chardet"
+version = "4.0.0"
+description = "Universal encoding detector for Python 2 and 3"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "click"
+version = "7.1.2"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "colorama"
+version = "0.4.4"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "coverage"
+version = "5.5"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.dependencies]
+toml = {version = "*", optional = true, markers = "extra == \"toml\""}
+
+[package.extras]
+toml = ["toml"]
+
+[[package]]
+name = "dataclasses"
+version = "0.8"
+description = "A backport of the dataclasses module for Python 3.6"
+category = "dev"
+optional = false
+python-versions = ">=3.6, <3.7"
+
+[[package]]
+name = "detect-secrets"
+version = "1.0.3"
+description = "Tool for detecting secrets in the codebase"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pyyaml = "*"
+requests = "*"
+
+[package.extras]
+word_list = ["pyahocorasick"]
+
+[[package]]
+name = "distlib"
+version = "0.3.1"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "filelock"
+version = "3.0.12"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "gitdb"
+version = "4.0.5"
+description = "Git Object Database"
+category = "dev"
+optional = false
+python-versions = ">=3.4"
+
+[package.dependencies]
+smmap = ">=3.0.1,<4"
+
+[[package]]
+name = "gitpython"
+version = "3.1.14"
+description = "Python Git Library"
+category = "dev"
+optional = false
+python-versions = ">=3.4"
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+
+[[package]]
+name = "identify"
+version = "1.6.2"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.extras]
+license = ["editdistance"]
+
+[[package]]
+name = "idna"
+version = "2.10"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "importlib-metadata"
+version = "3.7.3"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "importlib-resources"
+version = "5.1.2"
+description = "Read resources from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+zipp = {version = ">=0.4", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "isort"
+version = "5.8.0"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.extras]
+pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
+requirements_deprecated_finder = ["pipreqs", "pip-api"]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+
+[[package]]
+name = "lazy-object-proxy"
+version = "1.5.2"
+description = "A fast and thorough lazy object proxy."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mypy"
+version = "0.800"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3,<0.5.0"
+typed-ast = ">=1.4.0,<1.5.0"
+typing-extensions = ">=3.7.4"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "nodeenv"
+version = "1.5.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "packaging"
+version = "20.9"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+
+[[package]]
+name = "pathspec"
+version = "0.8.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pbr"
+version = "5.5.1"
+description = "Python Build Reasonableness"
+category = "dev"
+optional = false
+python-versions = ">=2.6"
+
+[[package]]
+name = "pluggy"
+version = "0.13.1"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+
+[[package]]
+name = "pre-commit"
+version = "1.21.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+"aspy.yaml" = "*"
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+importlib-resources = {version = "*", markers = "python_version < \"3.7\""}
+nodeenv = ">=0.11.1"
+pyyaml = "*"
+six = "*"
+toml = "*"
+virtualenv = ">=15.2"
+
+[[package]]
+name = "py"
+version = "1.10.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pylint"
+version = "2.7.2"
+description = "python code static checker"
+category = "dev"
+optional = false
+python-versions = "~=3.6"
+
+[package.dependencies]
+astroid = ">=2.5.1,<2.6"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+isort = ">=4.2.5,<6"
+mccabe = ">=0.6,<0.7"
+toml = ">=0.7.1"
+
+[package.extras]
+docs = ["sphinx (==3.5.1)", "python-docs-theme (==2020.12)"]
+
+[[package]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pyserial"
+version = "3.5"
+description = "Python Serial Port Extension"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+cp2110 = ["hidapi"]
+
+[[package]]
+name = "pytest"
+version = "6.2.2"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<1.0.0a1"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.14.0"
+description = "Pytest support for asyncio."
+category = "dev"
+optional = false
+python-versions = ">= 3.5"
+
+[package.dependencies]
+pytest = ">=5.4.0"
+
+[package.extras]
+testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"]
+
+[[package]]
+name = "pytest-cov"
+version = "2.11.1"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+coverage = ">=5.2.1"
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "pyyaml"
+version = "5.4.1"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[[package]]
+name = "regex"
+version = "2021.3.17"
+description = "Alternative regular expression module, to replace re."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "requests"
+version = "2.25.1"
+description = "Python HTTP for Humans."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+chardet = ">=3.0.2,<5"
+idna = ">=2.5,<3"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+
+[[package]]
+name = "six"
+version = "1.15.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "smmap"
+version = "3.0.5"
+description = "A pure Python implementation of a sliding window memory map manager"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "stevedore"
+version = "3.3.0"
+description = "Manage dynamic plugins for Python applications"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""}
+pbr = ">=2.0.0,<2.1.0 || >2.1.0"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "typed-ast"
+version = "1.4.2"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "3.7.4.3"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "urllib3"
+version = "1.26.4"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+brotli = ["brotlipy (>=0.6.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.4.3"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.dependencies]
+appdirs = ">=1.4.3,<2"
+distlib = ">=0.3.1,<1"
+filelock = ">=3.0.0,<4"
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""}
+six = ">=1.9.0,<2"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
+
+[[package]]
+name = "wrapt"
+version = "1.12.1"
+description = "Module for decorators, wrappers and monkey patching."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "zipp"
+version = "3.4.1"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.6"
+content-hash = "2d2711c15cb46922aa27a4e4606d310d6856c2950fdc09888ae452e250995493"
+
+[metadata.files]
+appdirs = [
+    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+"aspy.yaml" = [
+    {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"},
+    {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"},
+]
+astroid = [
+    {file = "astroid-2.5.1-py3-none-any.whl", hash = "sha256:21d735aab248253531bb0f1e1e6d068f0ee23533e18ae8a6171ff892b98297cf"},
+    {file = "astroid-2.5.1.tar.gz", hash = "sha256:cfc35498ee64017be059ceffab0a25bedf7548ab76f2bea691c5565896e7128d"},
+]
+async-timeout = [
+    {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
+    {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
+]
+atomicwrites = [
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+    {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
+    {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
+]
+bandit = [
+    {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
+    {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"},
+]
+black = [
+    {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
+]
+bump2version = [
+    {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"},
+    {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"},
+]
+certifi = [
+    {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
+    {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+]
+cfgv = [
+    {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"},
+    {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"},
+]
+chardet = [
+    {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
+    {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
+]
+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.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
+    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+]
+coverage = [
+    {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"},
+    {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"},
+    {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"},
+    {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"},
+    {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"},
+    {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"},
+    {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"},
+    {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"},
+    {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"},
+    {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"},
+    {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"},
+    {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"},
+    {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"},
+    {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"},
+    {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"},
+    {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"},
+    {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"},
+    {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"},
+    {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"},
+    {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"},
+    {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"},
+    {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"},
+    {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"},
+    {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"},
+    {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"},
+    {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"},
+    {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"},
+    {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
+    {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
+]
+dataclasses = [
+    {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
+    {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
+]
+detect-secrets = [
+    {file = "detect_secrets-1.0.3-py2.py3-none-any.whl", hash = "sha256:715d2351d1f0f18cf4e7861e74b4135af909652c07a79501abac86400c9d2164"},
+    {file = "detect_secrets-1.0.3.tar.gz", hash = "sha256:e2ea3f6b687a4d6ed4321f93ea7314ceceb383c643c85864ee38c0591ec42112"},
+]
+distlib = [
+    {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
+    {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
+]
+filelock = [
+    {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
+    {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
+]
+gitdb = [
+    {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"},
+    {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"},
+]
+gitpython = [
+    {file = "GitPython-3.1.14-py3-none-any.whl", hash = "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b"},
+    {file = "GitPython-3.1.14.tar.gz", hash = "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"},
+]
+identify = [
+    {file = "identify-1.6.2-py2.py3-none-any.whl", hash = "sha256:8f9879b5b7cca553878d31548a419ec2f227d3328da92fe8202bc5e546d5cbc3"},
+    {file = "identify-1.6.2.tar.gz", hash = "sha256:1c2014f6985ed02e62b2e6955578acf069cb2c54859e17853be474bfe7e13bed"},
+]
+idna = [
+    {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+    {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+]
+importlib-metadata = [
+    {file = "importlib_metadata-3.7.3-py3-none-any.whl", hash = "sha256:b74159469b464a99cb8cc3e21973e4d96e05d3024d337313fedb618a6e86e6f4"},
+    {file = "importlib_metadata-3.7.3.tar.gz", hash = "sha256:742add720a20d0467df2f444ae41704000f50e1234f46174b51f9c6031a1bd71"},
+]
+importlib-resources = [
+    {file = "importlib_resources-5.1.2-py3-none-any.whl", hash = "sha256:ebab3efe74d83b04d6bf5cd9a17f0c5c93e60fb60f30c90f56265fce4682a469"},
+    {file = "importlib_resources-5.1.2.tar.gz", hash = "sha256:642586fc4740bd1cad7690f836b3321309402b20b332529f25617ff18e8e1370"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+isort = [
+    {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"},
+    {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"},
+]
+lazy-object-proxy = [
+    {file = "lazy-object-proxy-1.5.2.tar.gz", hash = "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4"},
+    {file = "lazy_object_proxy-1.5.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e960e8be509e8d6d618300a6c189555c24efde63e85acaf0b14b2cd1ac743315"},
+    {file = "lazy_object_proxy-1.5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:522b7c94b524389f4a4094c4bf04c2b02228454ddd17c1a9b2801fac1d754871"},
+    {file = "lazy_object_proxy-1.5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3782931963dc89e0e9a0ae4348b44762e868ea280e4f8c233b537852a8996ab9"},
+    {file = "lazy_object_proxy-1.5.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:429c4d1862f3fc37cd56304d880f2eae5bd0da83bdef889f3bd66458aac49128"},
+    {file = "lazy_object_proxy-1.5.2-cp35-cp35m-win32.whl", hash = "sha256:cd1bdace1a8762534e9a36c073cd54e97d517a17d69a17985961265be6d22847"},
+    {file = "lazy_object_proxy-1.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ddbdcd10eb999d7ab292677f588b658372aadb9a52790f82484a37127a390108"},
+    {file = "lazy_object_proxy-1.5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ecb5dd5990cec6e7f5c9c1124a37cb2c710c6d69b0c1a5c4aa4b35eba0ada068"},
+    {file = "lazy_object_proxy-1.5.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b6577f15d5516d7d209c1a8cde23062c0f10625f19e8dc9fb59268859778d7d7"},
+    {file = "lazy_object_proxy-1.5.2-cp36-cp36m-win32.whl", hash = "sha256:c8fe2d6ff0ff583784039d0255ea7da076efd08507f2be6f68583b0da32e3afb"},
+    {file = "lazy_object_proxy-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:fa5b2dee0e231fa4ad117be114251bdfe6afe39213bd629d43deb117b6a6c40a"},
+    {file = "lazy_object_proxy-1.5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1d33d6f789697f401b75ce08e73b1de567b947740f768376631079290118ad39"},
+    {file = "lazy_object_proxy-1.5.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:57fb5c5504ddd45ed420b5b6461a78f58cbb0c1b0cbd9cd5a43ad30a4a3ee4d0"},
+    {file = "lazy_object_proxy-1.5.2-cp37-cp37m-win32.whl", hash = "sha256:e7273c64bccfd9310e9601b8f4511d84730239516bada26a0c9846c9697617ef"},
+    {file = "lazy_object_proxy-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f4e5e68b7af950ed7fdb594b3f19a0014a3ace0fedb86acb896e140ffb24302"},
+    {file = "lazy_object_proxy-1.5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cadfa2c2cf54d35d13dc8d231253b7985b97d629ab9ca6e7d672c35539d38163"},
+    {file = "lazy_object_proxy-1.5.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e7428977763150b4cf83255625a80a23dfdc94d43be7791ce90799d446b4e26f"},
+    {file = "lazy_object_proxy-1.5.2-cp38-cp38-win32.whl", hash = "sha256:2f2de8f8ac0be3e40d17730e0600619d35c78c13a099ea91ef7fb4ad944ce694"},
+    {file = "lazy_object_proxy-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:38c3865bd220bd983fcaa9aa11462619e84a71233bafd9c880f7b1cb753ca7fa"},
+    {file = "lazy_object_proxy-1.5.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:8a44e9901c0555f95ac401377032f6e6af66d8fc1fbfad77a7a8b1a826e0b93c"},
+    {file = "lazy_object_proxy-1.5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fa7fb7973c622b9e725bee1db569d2c2ee64d2f9a089201c5e8185d482c7352d"},
+    {file = "lazy_object_proxy-1.5.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:71a1ef23f22fa8437974b2d60fedb947c99a957ad625f83f43fd3de70f77f458"},
+    {file = "lazy_object_proxy-1.5.2-cp39-cp39-win32.whl", hash = "sha256:ef3f5e288aa57b73b034ce9c1f1ac753d968f9069cd0742d1d69c698a0167166"},
+    {file = "lazy_object_proxy-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:37d9c34b96cca6787fe014aeb651217944a967a5b165e2cacb6b858d2997ab84"},
+]
+mccabe = [
+    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+mypy = [
+    {file = "mypy-0.800-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:e1c84c65ff6d69fb42958ece5b1255394714e0aac4df5ffe151bc4fe19c7600a"},
+    {file = "mypy-0.800-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:947126195bfe4709c360e89b40114c6746ae248f04d379dca6f6ab677aa07641"},
+    {file = "mypy-0.800-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:b95068a3ce3b50332c40e31a955653be245666a4bc7819d3c8898aa9fb9ea496"},
+    {file = "mypy-0.800-cp35-cp35m-win_amd64.whl", hash = "sha256:ca7ad5aed210841f1e77f5f2f7d725b62c78fa77519312042c719ed2ab937876"},
+    {file = "mypy-0.800-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e32b7b282c4ed4e378bba8b8dfa08e1cfa6f6574067ef22f86bee5b1039de0c9"},
+    {file = "mypy-0.800-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e497a544391f733eca922fdcb326d19e894789cd4ff61d48b4b195776476c5cf"},
+    {file = "mypy-0.800-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:5615785d3e2f4f03ab7697983d82c4b98af5c321614f51b8f1034eb9ebe48363"},
+    {file = "mypy-0.800-cp36-cp36m-win_amd64.whl", hash = "sha256:2b216eacca0ec0ee124af9429bfd858d5619a0725ee5f88057e6e076f9eb1a7b"},
+    {file = "mypy-0.800-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e3b8432f8df19e3c11235c4563a7250666dc9aa7cdda58d21b4177b20256ca9f"},
+    {file = "mypy-0.800-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d16c54b0dffb861dc6318a8730952265876d90c5101085a4bc56913e8521ba19"},
+    {file = "mypy-0.800-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d2fc8beb99cd88f2d7e20d69131353053fbecea17904ee6f0348759302c52fa"},
+    {file = "mypy-0.800-cp37-cp37m-win_amd64.whl", hash = "sha256:aa9d4901f3ee1a986a3a79fe079ffbf7f999478c281376f48faa31daaa814e86"},
+    {file = "mypy-0.800-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:319ee5c248a7c3f94477f92a729b7ab06bf8a6d04447ef3aa8c9ba2aa47c6dcf"},
+    {file = "mypy-0.800-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:74f5aa50d0866bc6fb8e213441c41e466c86678c800700b87b012ed11c0a13e0"},
+    {file = "mypy-0.800-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a301da58d566aca05f8f449403c710c50a9860782148332322decf73a603280b"},
+    {file = "mypy-0.800-cp38-cp38-win_amd64.whl", hash = "sha256:b9150db14a48a8fa114189bfe49baccdff89da8c6639c2717750c7ae62316738"},
+    {file = "mypy-0.800-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5fdf935a46aa20aa937f2478480ebf4be9186e98e49cc3843af9a5795a49a25"},
+    {file = "mypy-0.800-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6f8425fecd2ba6007e526209bb985ce7f49ed0d2ac1cc1a44f243380a06a84fb"},
+    {file = "mypy-0.800-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5ff616787122774f510caeb7b980542a7cc2222be3f00837a304ea85cd56e488"},
+    {file = "mypy-0.800-cp39-cp39-win_amd64.whl", hash = "sha256:90b6f46dc2181d74f80617deca611925d7e63007cf416397358aa42efb593e07"},
+    {file = "mypy-0.800-py3-none-any.whl", hash = "sha256:3e0c159a7853e3521e3f582adb1f3eac66d0b0639d434278e2867af3a8c62653"},
+    {file = "mypy-0.800.tar.gz", hash = "sha256:e0202e37756ed09daf4b0ba64ad2c245d357659e014c3f51d8cd0681ba66940a"},
+]
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+nodeenv = [
+    {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"},
+    {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"},
+]
+packaging = [
+    {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
+    {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
+]
+pathspec = [
+    {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
+    {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
+]
+pbr = [
+    {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"},
+    {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"},
+]
+pluggy = [
+    {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+    {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+pre-commit = [
+    {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"},
+    {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"},
+]
+py = [
+    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
+pylint = [
+    {file = "pylint-2.7.2-py3-none-any.whl", hash = "sha256:d09b0b07ba06bcdff463958f53f23df25e740ecd81895f7d2699ec04bbd8dc3b"},
+    {file = "pylint-2.7.2.tar.gz", hash = "sha256:0e21d3b80b96740909d77206d741aa3ce0b06b41be375d92e1f3244a274c1f8a"},
+]
+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.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"},
+    {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"},
+]
+pytest = [
+    {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"},
+    {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"},
+]
+pytest-asyncio = [
+    {file = "pytest-asyncio-0.14.0.tar.gz", hash = "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"},
+    {file = "pytest_asyncio-0.14.0-py3-none-any.whl", hash = "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d"},
+]
+pytest-cov = [
+    {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
+    {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"},
+]
+pyyaml = [
+    {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
+    {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
+    {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
+    {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
+    {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
+]
+regex = [
+    {file = "regex-2021.3.17-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd"},
+    {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e"},
+    {file = "regex-2021.3.17-cp36-cp36m-win32.whl", hash = "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3"},
+    {file = "regex-2021.3.17-cp36-cp36m-win_amd64.whl", hash = "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5"},
+    {file = "regex-2021.3.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce"},
+    {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643"},
+    {file = "regex-2021.3.17-cp37-cp37m-win32.whl", hash = "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be"},
+    {file = "regex-2021.3.17-cp37-cp37m-win_amd64.whl", hash = "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb"},
+    {file = "regex-2021.3.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c"},
+    {file = "regex-2021.3.17-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d"},
+    {file = "regex-2021.3.17-cp38-cp38-win32.whl", hash = "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf"},
+    {file = "regex-2021.3.17-cp38-cp38-win_amd64.whl", hash = "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa"},
+    {file = "regex-2021.3.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106"},
+    {file = "regex-2021.3.17-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7"},
+    {file = "regex-2021.3.17-cp39-cp39-win32.whl", hash = "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd"},
+    {file = "regex-2021.3.17-cp39-cp39-win_amd64.whl", hash = "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38"},
+    {file = "regex-2021.3.17.tar.gz", hash = "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68"},
+]
+requests = [
+    {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
+    {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
+]
+six = [
+    {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
+    {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
+]
+smmap = [
+    {file = "smmap-3.0.5-py2.py3-none-any.whl", hash = "sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714"},
+    {file = "smmap-3.0.5.tar.gz", hash = "sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"},
+]
+stevedore = [
+    {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"},
+    {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"},
+]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+typed-ast = [
+    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
+    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
+    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
+    {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
+    {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
+    {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
+    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
+    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
+    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
+    {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
+    {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
+    {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
+    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
+    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
+    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
+    {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
+    {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
+    {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
+    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
+    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
+    {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
+    {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
+    {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
+    {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
+    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
+    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
+    {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
+    {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
+    {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
+    {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
+]
+typing-extensions = [
+    {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
+    {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
+    {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
+]
+urllib3 = [
+    {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"},
+    {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"},
+]
+virtualenv = [
+    {file = "virtualenv-20.4.3-py2.py3-none-any.whl", hash = "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"},
+    {file = "virtualenv-20.4.3.tar.gz", hash = "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107"},
+]
+wrapt = [
+    {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
+]
+zipp = [
+    {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"},
+    {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..eab8b59
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,66 @@
+[tool.poetry]
+name = "scpi"
+version = "2.2.0"
+description = "Basic idea here is to make transport-independent command sender/parser and a device baseclass that implements the common SCPI commands"
+authors = ["Eero af Heurlin <eero.afheurlin@iki.fi>"]
+homepage = "https://github.com/rambo/python-scpi/"
+repository = "https://github.com/rambo/python-scpi/"
+license = "LGPL"
+readme = "README.rst"
+
+[tool.black]
+line-length = 120
+target-version = ['py37']
+exclude = '''
+(
+  /(
+      \.eggs         # exclude a few common directories in the
+    | \.git          # root of the project
+    | \.hg
+    | \.mypy_cache
+    | \.tox
+    | \.venv
+    | _build
+    | buck-out
+    | build
+    | dist
+  )/
+  |  __pycache__
+)
+'''
+
+[tool.pytest.ini_options]
+junit_family="xunit2"
+addopts="--cov=scpi --cov-branch"
+
+[tool.pylint.format]
+max-line-length = 120
+
+[tool.pylint.messages_control]
+disable=["fixme", "W1202", "C0330"]
+
+[tool.coverage.run]
+omit = ["tests/*"]
+branch = true
+
+[tool.poetry.dependencies]
+python = "^3.6"
+pyserial = "^3.4"
+async-timeout = "^3.0"
+
+[tool.poetry.dev-dependencies]
+pytest = "^6.2"
+coverage = {version = "^5.4", extras = ["toml"]}
+pytest-cov = "^2.11"
+pylint = "^2.5"
+black = "=20.8b1"
+bandit = "^1.6"
+mypy = "^0.800"
+pre-commit = "^1.20"
+pytest-asyncio = "^0.14"
+bump2version = "^1.0"
+detect-secrets = "^1.0"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 7d2130d..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-pyserial==3.4
-async-timeout==3.0.0
diff --git a/setup.py b/setup.py
deleted file mode 100644
index a04e0c5..0000000
--- a/setup.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""Packaging script for python-scpi"""
-import os
-import subprocess
-import sys
-
-import setuptools
-
-if sys.version_info < (3, 5):
-    raise RuntimeError("Minimum version python 3.5")
-
-GIT_VERSION = 'UNKNOWN'
-try:
-    GIT_VERSION = subprocess.check_output(['git', 'rev-parse', '--verify', '--short', 'HEAD']).decode('ascii').strip()
-except subprocess.CalledProcessError:
-    pass
-
-setuptools.setup(
-    name='scpi',
-    version=os.getenv('PACKAGE_VERSION', '2.1.0+git.%s' % GIT_VERSION),
-    author='Eero "rambo" af Heurlin',
-    author_email='rambo@iki.fi',
-    packages=setuptools.find_packages(),
-    license='GNU LGPL',
-    long_description=open('README.md', 'rt', encoding='utf-8').read(),
-    long_description_content_type='text/markdown',
-    description='Implement SCPI in pure Python',
-    install_requires=open('requirements.txt', 'rt', encoding='utf-8').readlines(),
-    url='https://github.com/rambo/python-scpi',
-    classifiers=(
-        "Programming Language :: Python :: 3.5",
-        "Programming Language :: Python :: 3 :: Only",
-        "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2) ",
-    ),
-)
diff --git a/scpi/__init__.py b/src/scpi/__init__.py
similarity index 73%
rename from scpi/__init__.py
rename to src/scpi/__init__.py
index d4c4cf7..c2bcc02 100644
--- a/scpi/__init__.py
+++ b/src/scpi/__init__.py
@@ -1,4 +1,5 @@
 """SCPI module, the scpi class implements the base command set, devices may extend it.
 transports are separate from devices (so you can use for example hp6632b with either serial port or GPIB)"""
+__version__ = "2.2.0"  # NOTE Use `bump2version --config-file patch` to bump versions correctly
 from .scpi import SCPIProtocol, SCPIDevice
 from .errors import CommandError
diff --git a/scpi/devices/TDKLambdaZPlus.py b/src/scpi/devices/TDKLambdaZPlus.py
similarity index 93%
rename from scpi/devices/TDKLambdaZPlus.py
rename to src/scpi/devices/TDKLambdaZPlus.py
index 17916c5..d89801e 100644
--- a/scpi/devices/TDKLambdaZPlus.py
+++ b/src/scpi/devices/TDKLambdaZPlus.py
@@ -1,8 +1,8 @@
-'''
+"""
 Created on febrary 21 2020
 
 @author: qmor
-'''
+"""
 import decimal
 
 from ..scpi import SCPIDevice
@@ -11,7 +11,6 @@
 
 
 class TDKSCPI(SCPIDevice):
-
     async def set_power_on_status_clear(self, setting):
         """
         Set the Power-On Status Clear setting.
@@ -28,7 +27,6 @@ async def set_power_on_status_clear(self, setting):
             raise ValueError
         await super().set_power_on_status_clear(setting)
 
-
     async def restore_state(self, state):
         """
         Restores the power supply to a state previously stored in memory by *SAV command.
@@ -69,7 +67,6 @@ async def power_on_state(self, setting):
 
 
 class TDKLambdaZplus(PowerSupply, TDKSCPI):
-
     def __init__(self, protocol, use_safe_variants=True, voltage=20, current=10):
         """
         Initialize the device with voltage [V] and current [A] limits.
@@ -123,7 +120,7 @@ async def select_active_instrument(self, id):
         _id = int(id)
 
         if _id < 1 or id > 31:
-            raise ValueError('id %d is outside of the valid id range' % _id)
+            raise ValueError("id %d is outside of the valid id range" % _id)
 
         await self.command("INSTrument:NSELect %d" % _id)
 
@@ -226,7 +223,7 @@ async def set_voltage(self, millivolts, extra_params=""):
         Limited to five percent greater than the voltage limit of the unit.
         """
 
-        if millivolts/1000. > 1.05*self.voltage_limit or millivolts < 0:
+        if millivolts / 1000.0 > 1.05 * self.voltage_limit or millivolts < 0:
             raise ValueError
 
         await super().set_voltage(millivolts, extra_params=extra_params)
@@ -239,7 +236,7 @@ async def set_current(self, milliamps, extra_params=""):
         Limited to five percent greater than the current limit of the unit.
         """
 
-        if milliamps/1000. > 1.05*self.current_limit or milliamps < 0:
+        if milliamps / 1000.0 > 1.05 * self.current_limit or milliamps < 0:
             raise ValueError
 
         await super().set_current(milliamps, extra_params=extra_params)
@@ -249,6 +246,7 @@ def tcp(ip, port):
     """Quick helper to connect via TCP"""
     from ..transports.tcp import get as get_tcp
     from ..scpi import SCPIProtocol
+
     transport = get_tcp(ip, port)
     protocol = SCPIProtocol(transport)
     dev = TDKLambdaZplus(protocol)
@@ -260,15 +258,18 @@ def serial(port, baudrate=9600):
     from ..transports.rs232 import RS232Transport
     from ..scpi import SCPIProtocol
     import serial
-    port = serial.Serial(port,
-                         baudrate=baudrate,
-                         bytesize=8,
-                         parity=serial.PARITY_NONE,
-                         stopbits=1,
-                         xonxoff=False,
-                         rtscts=False,
-                         dsrdtr=False,
-                         timeout=10)
+
+    port = serial.Serial(
+        port,
+        baudrate=baudrate,
+        bytesize=8,
+        parity=serial.PARITY_NONE,
+        stopbits=1,
+        xonxoff=False,
+        rtscts=False,
+        dsrdtr=False,
+        timeout=10,
+    )
     transport = RS232Transport(port)
     protocol = SCPIProtocol(transport)
     dev = TDKLambdaZplus(protocol)
diff --git a/scpi/devices/__init__.py b/src/scpi/devices/__init__.py
similarity index 100%
rename from scpi/devices/__init__.py
rename to src/scpi/devices/__init__.py
diff --git a/scpi/devices/generic.py b/src/scpi/devices/generic.py
similarity index 100%
rename from scpi/devices/generic.py
rename to src/scpi/devices/generic.py
diff --git a/scpi/devices/hp6632b.py b/src/scpi/devices/hp6632b.py
similarity index 97%
rename from scpi/devices/hp6632b.py
rename to src/scpi/devices/hp6632b.py
index 810ca48..aa87e80 100644
--- a/scpi/devices/hp6632b.py
+++ b/src/scpi/devices/hp6632b.py
@@ -9,7 +9,7 @@ class HP6632B(PowerSupply, MultiMeter, SCPIDevice):
 
     async def set_low_current_mode(self, state):
         """The low-current mode is enabled by setting the range to (max) 20mA, anything over that is high-current mode.
-         This model has max 5A output"""
+        This model has max 5A output"""
         if state:
             return await self.set_measure_current_max(0.020)
         return await self.set_measure_current_max(5.0)
@@ -69,7 +69,7 @@ async def display_on(self, state=True):
     async def set_display_mode(self, mode):
         """Set the display mode, valied values are NORM and TEXT"""
         mode = mode.upper()
-        if mode not in ('NORM', 'TEXT'):
+        if mode not in ("NORM", "TEXT"):
             raise RuntimeError("Invalid mode %s, valid ones are NORM and TEXT" % mode)
         return await self.command("DISP:MODE %s" % mode)
 
@@ -78,7 +78,7 @@ async def set_display_text(self, text):
         NOTE: does *not* set display mode, you need to do it yourself"""
         if len(text) > 14:
             raise RuntimeError("Max text length is 14 characters")
-        if ('"' in text and "'" in text):
+        if '"' in text and "'" in text:
             raise RuntimeError("Text may only contain either single or double quotes, not both")
         if '"' in text:
             return await self.command("DISP:TEXT '%s'" % text)
@@ -89,6 +89,7 @@ def rs232(port, **kwargs):
     """Quick helper to connect via RS232 port"""
     from ..transports.rs232 import get as get_rs232
     from ..scpi import SCPIProtocol
+
     transport = get_rs232(port, **kwargs)
     protocol = SCPIProtocol(transport)
     dev = HP6632B(protocol)
diff --git a/scpi/errors/__init__.py b/src/scpi/errors/__init__.py
similarity index 100%
rename from scpi/errors/__init__.py
rename to src/scpi/errors/__init__.py
diff --git a/scpi/scpi.py b/src/scpi/scpi.py
similarity index 92%
rename from scpi/scpi.py
rename to src/scpi/scpi.py
index 0006791..e173e17 100644
--- a/scpi/scpi.py
+++ b/src/scpi/scpi.py
@@ -14,6 +14,7 @@
 
 class BitEnum(object):
     """Baseclass for bit definitions of various status registers"""
+
     @classmethod
     def test_bit(cls, statusvalue, bitname):
         """Test if the given status value has the given bit set"""
@@ -132,8 +133,10 @@ def error_available(self):
 
 class SCPIProtocol(object):
     """Implements the SCPI protocol talks over the given transport"""
+
     transport = None
     lock = asyncio.Lock()
+    checking_error = False
 
     def __init__(self, transport):
         self.transport = transport
@@ -148,16 +151,22 @@ async def abort_command(self):
 
     async def get_error(self):
         """Asks for the error code and string"""
-        response = await self.ask('SYST:ERR?')
-        match = ERROR_RE.search(response)
-        if not match:
-            # PONDER: Make our own exceptions ??
-            raise ValueError("Response '{:s}' does not have correct error format".format(response))
-        code = int(match.group(1))
-        errstr = match.group(2)
-        return (code, errstr)
-
-    async def check_error(self, prev_command=''):
+        if self.checking_error:
+            raise RuntimeError("Recursion on get_error detected")
+        try:
+            self.checking_error = True
+            response = await self.ask("SYST:ERR?")
+            match = ERROR_RE.search(response)
+            if not match:
+                # PONDER: Make our own exceptions ??
+                raise ValueError("Response '{:s}' does not have correct error format".format(response))
+            code = int(match.group(1))
+            errstr = match.group(2)
+            return (code, errstr)
+        finally:
+            self.checking_error = False
+
+    async def check_error(self, prev_command=""):
         """Check for error and raise exception if present"""
         code, errstr = await self.get_error()
         if code != 0:
@@ -167,7 +176,7 @@ async def command(self, command, cmd_timeout=COMMAND_DEFAULT_TIMEOUT, abort_on_t
         """Sends a command, does not wait for response"""
         try:
             with timeout(cmd_timeout):
-                with (await self.lock):
+                async with self.lock:
                     await self.transport.send_command(command)
         except (asyncio.TimeoutError, asyncio.CancelledError) as err:
             # check for the actual error if available
@@ -187,7 +196,7 @@ async def ask(self, command, cmd_timeout=COMMAND_DEFAULT_TIMEOUT, abort_on_timeo
         """Send a command and waits for response, returns the response"""
         try:
             with timeout(cmd_timeout):
-                with (await self.lock):
+                async with self.lock:
                     await self.transport.send_command(command)
                     return await self.transport.get_response()
 
@@ -211,6 +220,7 @@ class SCPIDevice(object):
     """Implements nicer wrapper methods for the raw commands from the generic SCPI command set
 
     See also devices.mixins for mixin classes with more features"""
+
     protocol = None
     transport = None
     command = None
@@ -249,18 +259,18 @@ async def get_error(self):
 
     async def reset(self):
         """Resets the device to known state (with *RST) and clears the error log"""
-        return await self.protocol.command('*RST;*CLS')
+        return await self.protocol.command("*RST;*CLS")
 
     async def wait_for_complete(self, wait_timeout):
         """Wait for all queued operations to complete (up-to defined timeout)"""
-        resp = await self.ask('*WAI;*OPC?', cmd_timeout=wait_timeout)
+        resp = await self.ask("*WAI;*OPC?", cmd_timeout=wait_timeout)
         return bool(int(resp))
 
     async def identify(self):
         """Returns the identification data, standard order is:
-         Manufacturer, Model no, Serial no (or 0), Firmware version"""
+        Manufacturer, Model no, Serial no (or 0), Firmware version"""
         resp = await self.ask("*IDN?")
-        return resp.split(',')
+        return resp.split(",")
 
     async def query_esr(self):
         """Queries the event status register (ESR) NOTE: The register is cleared when read!
diff --git a/scpi/transports/__init__.py b/src/scpi/transports/__init__.py
similarity index 54%
rename from scpi/transports/__init__.py
rename to src/scpi/transports/__init__.py
index 91c7fb5..639a66f 100644
--- a/scpi/transports/__init__.py
+++ b/src/scpi/transports/__init__.py
@@ -1,6 +1,3 @@
 """Transport layers for the SCPI module"""
-try:
-    from .rs232 import RS232Transport
-except:
-    pass
+from .rs232 import RS232Transport
 from .tcp import TCPTransport
diff --git a/scpi/transports/baseclass.py b/src/scpi/transports/baseclass.py
similarity index 99%
rename from scpi/transports/baseclass.py
rename to src/scpi/transports/baseclass.py
index 17f6edc..18b5f9d 100644
--- a/scpi/transports/baseclass.py
+++ b/src/scpi/transports/baseclass.py
@@ -15,6 +15,7 @@ class AbstractTransport(object):
 
 class BaseTransport(AbstractTransport):
     """Baseclass for SCPI tranport layers, abstracts away details, must be subclasses to implement"""
+
     message_callback = None
     unsolicited_message_callback = None
     lock = asyncio.Lock()
diff --git a/scpi/transports/gpib/__init__.py b/src/scpi/transports/gpib/__init__.py
similarity index 100%
rename from scpi/transports/gpib/__init__.py
rename to src/scpi/transports/gpib/__init__.py
diff --git a/scpi/transports/gpib/base.py b/src/scpi/transports/gpib/base.py
similarity index 99%
rename from scpi/transports/gpib/base.py
rename to src/scpi/transports/gpib/base.py
index d41e12a..0cdffba 100644
--- a/scpi/transports/gpib/base.py
+++ b/src/scpi/transports/gpib/base.py
@@ -54,6 +54,7 @@ def get_device_transport(self, address, secondary=None):
 
 class GPIBDeviceTransport(AbstractTransport):
     """Device specific transport, handles addressing transparently"""
+
     my_address = None
     lltransport = None
 
diff --git a/scpi/transports/gpib/prologix.py b/src/scpi/transports/gpib/prologix.py
similarity index 78%
rename from scpi/transports/gpib/prologix.py
rename to src/scpi/transports/gpib/prologix.py
index c98146e..6e5765c 100644
--- a/scpi/transports/gpib/prologix.py
+++ b/src/scpi/transports/gpib/prologix.py
@@ -14,11 +14,13 @@
 
 class PrologixRS232SerialProtocol(RS232SerialProtocol):
     """Basically the same deal as with the stock RS232 PySerial "protocol" but different EOL"""
-    TERMINATOR = b'\n'
+
+    TERMINATOR = b"\n"
 
 
 class PrologixGPIBTransport(GPIBTransport):
     """Transport "driver" for the Prologix USB-GPIB controller (v6 protocol)"""
+
     serialhandler = None
 
     def __init__(self, serial_device, *args, **kwargs):
@@ -31,42 +33,43 @@ def __init__(self, serial_device, *args, **kwargs):
     def initialize_controller(self):
         """Initializes the controller to known state"""
         # Set to controller mode
-        self.serialhandler.protocol.write_line('++mode 1')
+        self.serialhandler.protocol.write_line("++mode 1")
         # Disable automatic read after write
-        self.serialhandler.protocol.write_line('++auto 0')
+        self.serialhandler.protocol.write_line("++auto 0")
         # Auto-assert End Of Instruction after commands
-        self.serialhandler.protocol.write_line('++eoi 1')
+        self.serialhandler.protocol.write_line("++eoi 1")
         # Append CRLF to device commands (EOI above *should* be enough but this is probably more compatible)
-        self.serialhandler.protocol.write_line('++eos 0')
+        self.serialhandler.protocol.write_line("++eos 0")
         # We do not have parsing support for the EOT character so disable it
-        self.serialhandler.protocol.write_line('++eot_enable 0')
+        self.serialhandler.protocol.write_line("++eot_enable 0")
         # Set inter-character timeout for read commands
-        self.serialhandler.protocol.write_line('++read_tmo_ms 500')
+        self.serialhandler.protocol.write_line("++read_tmo_ms 500")
         # Assert IFC, make us Controller In Charge
-        self.serialhandler.protocol.write_line('++ifc')
+        self.serialhandler.protocol.write_line("++ifc")
 
     async def send_command(self, command):
         """Wrapper for write_line on the protocol with some sanity checks"""
         if not self.serialhandler or not self.serialhandler.is_alive():
             raise RuntimeError("Serial handler not ready")
-        with (await self.lock):
+        async with self.lock:
             self.serialhandler.protocol.write_line(command)
 
     async def get_response(self):
         """Get device response"""
-        return await self.send_and_read('++read eoi')
+        return await self.send_and_read("++read eoi")
 
     async def send_and_read(self, send):
         """Send a line, read the response. NOTE: This is for talking with the controller, device responses
         need to use get_response as usual"""
         with timeout(READ_TIMEOUT):
-            with (await self.lock):
+            async with self.lock:
                 response = None
 
                 def set_response(message):
                     """Callback for setting the response"""
                     nonlocal response
                     response = message
+
                 self.message_callback = set_response
                 self.serialhandler.protocol.write_line(send)
                 while response is None:
@@ -76,9 +79,9 @@ def set_response(message):
     async def set_address(self, address, secondary=None):
         """Set the address we want to talk to"""
         if secondary is None:
-            await self.send_command('++addr %d' % address)
+            await self.send_command("++addr %d" % address)
         else:
-            await self.send_command('++addr %d %d' % (address, secondary))
+            await self.send_command("++addr %d %d" % (address, secondary))
         # Wait for the address to actually be set
         while True:
             await asyncio.sleep(0)
@@ -89,8 +92,8 @@ async def set_address(self, address, secondary=None):
     async def query_address(self):
         """Query the address we are talking to, returns tuple with primary and secondary parts
         secondary is None if not set"""
-        resp = await self.send_and_read('++addr')
-        parts = resp.split(' ')
+        resp = await self.send_and_read("++addr")
+        parts = resp.split(" ")
         address = int(parts[0])
         secondary = None
         if len(parts) > 1:
@@ -99,28 +102,28 @@ async def query_address(self):
 
     async def send_scd(self):
         """Sends the Selected Device Clear (SDC) message to the currently specified GPIB address"""
-        await self.send_command('++clr')
+        await self.send_command("++clr")
 
     async def send_ifc(self):
         """Asserts GPIB IFC signal"""
-        await self.send_command('++ifc')
+        await self.send_command("++ifc")
 
     async def send_llo(self):
         """Send LLO (disable front panel) to currently specified address"""
-        await self.send_command('++llo')
+        await self.send_command("++llo")
 
     async def send_loc(self):
         """Send LOC (enable front panel) to currently specified address"""
-        await self.send_command('++loc')
+        await self.send_command("++loc")
 
     async def get_srq(self):
         """Get SRQ assertion status"""
-        resp = await self.send_and_read('++srq')
+        resp = await self.send_and_read("++srq")
         return int(resp)
 
     async def poll(self):
         """Do serial poll on the selected device"""
-        resp = await self.send_and_read('++spoll')
+        resp = await self.send_and_read("++spoll")
         return int(resp)
 
     async def send_group_trig(self, addresses=None):
@@ -129,8 +132,8 @@ async def send_group_trig(self, addresses=None):
         For some reason Prologix does not trigger the whole bus but only listed devices (if none listed then
         the currently selected device is used)"""
         if addresses is None:
-            return await self.send_command('++trg')
-        await self.send_command('++trg ' + ' '.join((str(x) for x in addresses)))
+            return await self.send_command("++trg")
+        await self.send_command("++trg " + " ".join((str(x) for x in addresses)))
 
     async def scan_devices(self):
         """Scan for devices in the bus.
@@ -138,8 +141,8 @@ async def scan_devices(self):
         found_addresses = []
         # We do not lock on this level since the commands we use need to manipulate the lock
         prev_addr = await self.query_address()
-        prev_read_tmo_ms = await self.send_and_read('++read_tmo_ms')
-        self.serialhandler.protocol.write_line('++read_tmo_ms %d' % int((SCAN_DEVICE_TIMEOUT / 2) * 1000))
+        prev_read_tmo_ms = await self.send_and_read("++read_tmo_ms")
+        self.serialhandler.protocol.write_line("++read_tmo_ms %d" % int((SCAN_DEVICE_TIMEOUT / 2) * 1000))
         for addr in range(0, 31):  # 0-30 inclusive
             with timeout(SCAN_DEVICE_TIMEOUT):
                 try:
@@ -148,14 +151,14 @@ async def scan_devices(self):
                     found_addresses.append(addr)
                 except (asyncio.TimeoutError, asyncio.CancelledError):
                     pass
-        self.serialhandler.protocol.write_line('++read_tmo_ms ' + prev_read_tmo_ms)
+        self.serialhandler.protocol.write_line("++read_tmo_ms " + prev_read_tmo_ms)
         # Wait a moment for things to settle
-        await asyncio.sleep(float(prev_read_tmo_ms)/1000)
+        await asyncio.sleep(float(prev_read_tmo_ms) / 1000)
         # Get ids for the devices we found
         ret = []
         for addr in found_addresses:
             await self.set_address(addr)
-            await self.send_command('*IDN?')
+            await self.send_command("*IDN?")
             idstr = await self.get_response()
             ret.append((addr, idstr))
         await self.set_address(*prev_addr)
diff --git a/scpi/transports/rs232.py b/src/scpi/transports/rs232.py
similarity index 96%
rename from scpi/transports/rs232.py
rename to src/scpi/transports/rs232.py
index 5a6e1f2..bc473d2 100644
--- a/scpi/transports/rs232.py
+++ b/src/scpi/transports/rs232.py
@@ -11,7 +11,8 @@
 
 class RS232SerialProtocol(serial.threaded.LineReader):
     """PySerial "protocol" class for handling stuff"""
-    ENCODING = 'ascii'
+
+    ENCODING = "ascii"
 
     def connection_made(self, transport):
         """Overridden to make sure we have write_timeout set"""
@@ -25,6 +26,7 @@ def handle_line(self, line):
 
 class RS232Transport(BaseTransport):
     """Uses PySerials ReaderThread in the background to save us some pain"""
+
     serialhandler = None
 
     def __init__(self, serial_device, *args, **kwargs):
@@ -37,20 +39,21 @@ async def send_command(self, command):
         """Wrapper for write_line on the protocol with some sanity checks"""
         if not self.serialhandler or not self.serialhandler.is_alive():
             raise RuntimeError("Serial handler not ready")
-        with (await self.lock):
+        async with self.lock:
             self.serialhandler.protocol.write_line(command)
 
     async def get_response(self):
         """Serial devices send responses without needing to be told to, just reads it"""
         # TODO: we probably have a race-condition possibility here, maybe always put all received
         # messages to a stack and return popleft ??
-        with (await self.lock):
+        async with self.lock:
             response = None
 
             def set_response(message):
                 """Callback for setting the response"""
                 nonlocal response
                 response = message
+
             self.message_callback = set_response
             while response is None:
                 await asyncio.sleep(0)
@@ -62,7 +65,7 @@ async def abort_command(self):
         clear message is received. Device clear performs the following actions:
              - The input and output buffers of the dc source are cleared.
              - The dc source is prepared to accept a new command string."""
-        with (await self.lock):
+        async with self.lock:
             self.serialhandler.serial.send_break()
 
     async def quit(self):
diff --git a/scpi/transports/tcp.py b/src/scpi/transports/tcp.py
similarity index 71%
rename from scpi/transports/tcp.py
rename to src/scpi/transports/tcp.py
index 66e3aa6..e6b0e98 100644
--- a/scpi/transports/tcp.py
+++ b/src/scpi/transports/tcp.py
@@ -1,33 +1,33 @@
-'''
+"""
 Created on febrary 21 2020
 
 @author: qmor
-'''
+"""
 import asyncio
 from .baseclass import BaseTransport
 
+
 class TCPTransport(BaseTransport):
-    async def openconnection(self, ip,port):
-        self.reader, self.writer = await asyncio.open_connection(ip, port, loop= asyncio.get_event_loop())
+    async def openconnection(self, ip, port):
+        self.reader, self.writer = await asyncio.open_connection(ip, port, loop=asyncio.get_event_loop())
 
     def __init__(self, ip, port, *args, **kwargs):
         super().__init__(*args, **kwargs)
         loop = asyncio.get_event_loop()
         loop.run_until_complete(self.openconnection(ip, port))
-        
-    
+
     async def send_command(self, command):
-        with (await self.lock):
-            print (command)
-            self.writer.write((command+"\r\n").encode())
+        async with self.lock:
+            print(command)
+            self.writer.write((command + "\r\n").encode())
             await asyncio.sleep(0.05)
             await self.writer.drain()
 
     async def get_response(self):
-        with (await self.lock):
+        async with self.lock:
             data = await self.reader.readline()
-            res=data.decode()
-            print (res.strip())
+            res = data.decode()
+            print(res.strip())
         return res
 
     async def quit(self):
@@ -38,4 +38,4 @@ async def quit(self):
 
 def get(ip, port):
     """Shorthand for creating the port from ip and port and initializing the transport"""
-    return TCPTransport(ip, port)
\ No newline at end of file
+    return TCPTransport(ip, port)
diff --git a/scpi/wrapper.py b/src/scpi/wrapper.py
similarity index 99%
rename from scpi/wrapper.py
rename to src/scpi/wrapper.py
index 6e4bed7..b71cf08 100644
--- a/scpi/wrapper.py
+++ b/src/scpi/wrapper.py
@@ -6,6 +6,7 @@
 
 class AIOWrapper(object):
     """Wraps all coroutine methods into asyncio run_until_complete calls"""
+
     _device = None
     _loop = None
 
@@ -25,11 +26,13 @@ def __init__(self, device):
     def __getattr__(self, item):
         orig = getattr(self._device, item)
         if inspect.iscoroutinefunction(orig):
+
             @functools.wraps(orig)
             def wrapped(*args, **kwargs):
                 """Gets the waitable and tells the event loop to run it"""
                 waitable = orig(*args, **kwargs)
                 return self._loop.run_until_complete(waitable)
+
             return wrapped
         return orig
 
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..4530038
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,15 @@
+[tox]
+isolated_build = true
+;pylint has issue with Optional and Union type hints on 3.9, re-enable when fixed
+;envlist = py39,py38,py37
+envlist = py38,py37
+
+[testenv]
+whitelist_externals = poetry
+passenv = SSH_AUTH_SOCK SKIP
+commands =
+    poetry install -v
+    poetry run docker/pre_commit_init.sh  # this also checks all files
+    poetry run pytest --junitxml=pytest-{envname}.xml tests/
+    poetry run mypy --strict src tests
+    poetry run bandit -r src


Test patch:
diff --git a/docker/entrypoint-test.sh b/docker/entrypoint-test.sh
new file mode 100755
index 0000000..2ec55cc
--- /dev/null
+++ b/docker/entrypoint-test.sh
@@ -0,0 +1,16 @@
+#!/bin/bash -l
+set -e
+if [ "$#" -eq 0 ]; then
+  # Kill cache, pytest complains about it if running local and docker tests in mapped volume
+  find tests  -type d -name '__pycache__' -print0 | xargs -0 rm -rf {}
+  # Make sure the service itself is installed
+  poetry install
+  # Make sure pre-commit checks were not missed because reasons
+  pre-commit run --all-files
+  # Then run the tests
+  pytest --junitxml=pytest.xml tests/
+  mypy --strict src tests
+  bandit -r src
+else
+  exec "$@"
+fi
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..8813124
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,1 @@
+"""Tests for datastreamservicelib"""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..77d23ed
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,1 @@
+"""pytest automagics"""
diff --git a/tests/test_scpi.py b/tests/test_scpi.py
new file mode 100644
index 0000000..f4f748c
--- /dev/null
+++ b/tests/test_scpi.py
@@ -0,0 +1,7 @@
+"""Package level tests"""
+from scpi import __version__
+
+
+def test_version() -> None:
+    """Make sure version matches expected"""
+    assert __version__ == "2.2.0"