From 317da3d2f1c713278cd0ab11c12d05628d2e48d7 Mon Sep 17 00:00:00 2001
From: James Boyle <12632889+jhhb@users.noreply.github.com>
Date: Fri, 4 Jun 2021 08:43:35 -0400
Subject: [PATCH] 0.0.0-pre . Working library, valid docs, for all providers.
(#2)
---
.gitattributes | 2 +
.gitignore | 163 +---
.isort.cfg | 10 +
.mypy.ini | 7 +
.pydocstyle.ini | 14 +
.pylint.ini | 503 ++++++++++
.tool-versions | 1 +
.travis.yml | 36 +
.verchew.ini | 22 +
CHANGELOG.md | 3 +
CONTRIBUTING.md | 84 ++
LICENSE | 695 +-------------
Makefile | 206 +++++
README.md | 111 ++-
README.todo.md | 12 +
bin/checksum | 23 +
bin/open | 22 +
bin/update | 80 ++
bin/verchew | 366 ++++++++
defipulsedata/__init__.py | 5 +
defipulsedata/defi_pulse.py | 88 ++
defipulsedata/dex_ag.py | 58 ++
defipulsedata/eth_gas_station.py | 21 +
defipulsedata/pools_fyi.py | 84 ++
defipulsedata/rek_to.py | 29 +
defipulsedata/utils.py | 30 +
docs/about/changelog.md | 1 +
docs/about/contributing.md | 1 +
docs/about/endpoints.md | 1 +
docs/about/license.md | 1 +
docs/index.md | 1 +
docs/requirements.txt | 2 +
endpoints.md | 43 +
mkdocs.yml | 19 +
poetry.lock | 1461 ++++++++++++++++++++++++++++++
pyproject.toml | 78 ++
pytest.ini | 15 +
scent.py | 95 ++
tests/__init__.py | 1 +
tests/conftest.py | 11 +
tests/test_defi_pulse.py | 127 +++
tests/test_dex_ag.py | 124 +++
tests/test_eth_gas_station.py | 23 +
tests/test_pools_fyi.py | 84 ++
tests/test_rek_to.py | 41 +
tests/test_utils.py | 47 +
46 files changed, 4054 insertions(+), 797 deletions(-)
create mode 100644 .gitattributes
create mode 100644 .isort.cfg
create mode 100644 .mypy.ini
create mode 100644 .pydocstyle.ini
create mode 100644 .pylint.ini
create mode 100644 .tool-versions
create mode 100644 .travis.yml
create mode 100644 .verchew.ini
create mode 100644 CHANGELOG.md
create mode 100644 CONTRIBUTING.md
create mode 100644 Makefile
create mode 100644 README.todo.md
create mode 100755 bin/checksum
create mode 100755 bin/open
create mode 100755 bin/update
create mode 100755 bin/verchew
create mode 100644 defipulsedata/__init__.py
create mode 100644 defipulsedata/defi_pulse.py
create mode 100644 defipulsedata/dex_ag.py
create mode 100644 defipulsedata/eth_gas_station.py
create mode 100644 defipulsedata/pools_fyi.py
create mode 100644 defipulsedata/rek_to.py
create mode 100644 defipulsedata/utils.py
create mode 120000 docs/about/changelog.md
create mode 120000 docs/about/contributing.md
create mode 120000 docs/about/endpoints.md
create mode 120000 docs/about/license.md
create mode 120000 docs/index.md
create mode 100644 docs/requirements.txt
create mode 100644 endpoints.md
create mode 100644 mkdocs.yml
create mode 100644 poetry.lock
create mode 100644 pyproject.toml
create mode 100644 pytest.ini
create mode 100644 scent.py
create mode 100644 tests/__init__.py
create mode 100644 tests/conftest.py
create mode 100644 tests/test_defi_pulse.py
create mode 100644 tests/test_dex_ag.py
create mode 100644 tests/test_eth_gas_station.py
create mode 100644 tests/test_pools_fyi.py
create mode 100644 tests/test_rek_to.py
create mode 100644 tests/test_utils.py
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..201aadb
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto
+CHANGELOG.md merge=union
diff --git a/.gitignore b/.gitignore
index b6e4761..b97f9c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,129 +1,50 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# 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
-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
-.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
+# Temporary Python files
+*.pyc
+*.egg-info
+__pycache__
.ipynb_checkpoints
+setup.py
+pip-wheel-metadata/
-# 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
+# Temporary OS files
+Icon*
-# SageMath parsed files
-*.sage.py
+# Temporary virtual environment files
+/.cache/
+/.venv/
-# Environments
+# Temporary server files
.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
+*.pid
+
+# Generated documentation
+/docs/gen/
+/docs/apidocs/
+/site/
+/*.html
+/docs/*.png
+
+# Google Drive
+*.gdoc
+*.gsheet
+*.gslides
+*.gdraw
+
+# Testing and coverage results
+/.coverage
+/.coverage.*
+/htmlcov/
+
+# Build and release directories
+/build/
+/dist/
+*.spec
-# mkdocs documentation
-/site
+# Sublime Text
+*.sublime-workspace
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
+# Eclipse
+.settings
-# Pyre type checker
-.pyre/
+# project-specific
+defipulsedata/repl.py
\ No newline at end of file
diff --git a/.isort.cfg b/.isort.cfg
new file mode 100644
index 0000000..91e9e3c
--- /dev/null
+++ b/.isort.cfg
@@ -0,0 +1,10 @@
+[settings]
+
+multi_line_output = 3
+
+combine_as_imports = true
+force_grid_wrap = false
+include_trailing_comma = true
+
+lines_after_imports = 2
+line_length = 88
diff --git a/.mypy.ini b/.mypy.ini
new file mode 100644
index 0000000..264105f
--- /dev/null
+++ b/.mypy.ini
@@ -0,0 +1,7 @@
+[mypy]
+
+ignore_missing_imports = true
+no_implicit_optional = true
+check_untyped_defs = true
+
+cache_dir = .cache/mypy/
diff --git a/.pydocstyle.ini b/.pydocstyle.ini
new file mode 100644
index 0000000..69e38cb
--- /dev/null
+++ b/.pydocstyle.ini
@@ -0,0 +1,14 @@
+[pydocstyle]
+
+# D211: No blank lines allowed before class docstring
+add_select = D211
+
+# D100: Missing docstring in public module
+# D101: Missing docstring in public class
+# D102: Missing docstring in public method
+# D103: Missing docstring in public function
+# D104: Missing docstring in public package
+# D105: Missing docstring in magic method
+# D107: Missing docstring in __init__
+# D202: No blank lines allowed after function docstring
+add_ignore = D100,D101,D102,D103,D104,D105,D107,D202
diff --git a/.pylint.ini b/.pylint.ini
new file mode 100644
index 0000000..2f6791f
--- /dev/null
+++ b/.pylint.ini
@@ -0,0 +1,503 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=
+ print-statement,
+ parameter-unpacking,
+ unpacking-in-except,
+ old-raise-syntax,
+ backtick,
+ long-suffix,
+ old-ne-operator,
+ old-octal-literal,
+ import-star-module-level,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ locally-enabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ xrange-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ raising-string,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ deprecated-string-function,
+ deprecated-str-translate-call,
+ missing-docstring,
+ invalid-name,
+ too-few-public-methods,
+ fixme,
+ too-many-arguments,
+ too-many-branches,
+ unpacking-non-sequence,
+ wildcard-import,
+ unused-wildcard-import,
+ singleton-comparison,
+ bad-continuation,
+ wrong-import-order,
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=no
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[BASIC]
+
+# Naming hint for argument names
+argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct argument names
+argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Naming hint for attribute names
+attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct attribute names
+attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Naming hint for class attribute names
+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Naming hint for class names
+class-name-hint=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Naming hint for constant names
+const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming hint for function names
+function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct function names
+function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Naming hint for inline iteration names
+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Naming hint for method names
+method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct method names
+method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Naming hint for module names
+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming hint for variable names
+variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^.*((https?:)|(pragma:)|(TODO:)).*$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=88
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,future.builtins
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000..812fafa
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1 @@
+python 3.7.10
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..8de5613
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,36 @@
+dist: xenial
+
+language: python
+python:
+ - 3.7
+
+cache:
+ pip: true
+ directories:
+ - ${VIRTUAL_ENV}
+
+env:
+ global:
+ - RANDOM_SEED=0
+
+before_install:
+ - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
+ - source $HOME/.poetry/env
+ - make doctor
+
+install:
+ - make install
+
+script:
+ - make check
+ - make test
+
+after_success:
+ - pip install coveralls scrutinizer-ocular
+ - coveralls
+ - ocular
+
+notifications:
+ email:
+ on_success: never
+ on_failure: never
diff --git a/.verchew.ini b/.verchew.ini
new file mode 100644
index 0000000..0d7970b
--- /dev/null
+++ b/.verchew.ini
@@ -0,0 +1,22 @@
+[Make]
+
+cli = make
+version = GNU Make
+
+[Python]
+
+cli = python
+version = 3.7
+
+[Poetry]
+
+cli = poetry
+version = 1
+
+[Graphviz]
+
+cli = dot
+cli_version_arg = -V
+version = 2
+optional = true
+message = This is only needed to generate UML diagrams for documentation.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0a691af
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.0.0 (YYYY-MM-DD)
+
+ - TBD
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b246477
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,84 @@
+# Setup
+
+## Requirements
+
+* Make:
+ * macOS: `$ xcode-select --install`
+ * Linux: [https://www.gnu.org/software/make](https://www.gnu.org/software/make)
+ * Windows: [https://mingw.org/download/installer](https://mingw.org/download/installer)
+* Python: `$ pyenv install`
+* Poetry: [https://poetry.eustace.io/docs/#installation](https://poetry.eustace.io/docs/#installation)
+* Graphviz:
+ * macOS: `$ brew install graphviz`
+ * Linux: [https://graphviz.org/download](https://graphviz.org/download/)
+ * Windows: [https://graphviz.org/download](https://graphviz.org/download/)
+
+To confirm these system dependencies are configured correctly:
+
+```text
+$ make doctor
+```
+
+## Installation
+
+Install project dependencies into a virtual environment:
+
+```text
+$ make install
+```
+
+# Development Tasks
+
+## Manual
+
+Run the tests:
+
+```text
+$ make test
+```
+
+Run static analysis:
+
+```text
+$ make check
+```
+
+Build the documentation:
+
+```text
+$ make docs
+```
+
+## Automatic
+
+Keep all of the above tasks running on change:
+
+```text
+$ make watch
+```
+
+> In order to have OS X notifications, `brew install terminal-notifier`.
+
+# Continuous Integration
+
+The CI server will report overall build status:
+
+```text
+$ make ci
+```
+
+# Demo Tasks
+
+Run the program:
+
+```text
+$ make run
+````
+
+# Release Tasks
+
+Release to PyPI:
+
+```text
+$ make upload
+```
diff --git a/LICENSE b/LICENSE
index f288702..3bb442a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,674 +1,21 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
+**The MIT License (MIT)**
+
+Copyright © 2021, James Boyle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e87e81e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,206 @@
+PACKAGE := defipulsedata
+MODULES := $(wildcard $(PACKAGE)/*.py)
+
+# MAIN TASKS ##################################################################
+
+.PHONY: all
+all: install
+
+.PHONY: ci
+ci: format check test mkdocs ## Run all tasks that determine CI status
+
+.PHONY: watch
+watch: install .clean-test ## Continuously run all CI tasks when files chanage
+ poetry run sniffer
+
+.PHONY: run ## Start the program
+run: install
+ poetry run python $(PACKAGE)/__init__.py
+
+# SYSTEM DEPENDENCIES #########################################################
+
+.PHONY: doctor
+doctor: ## Confirm system dependencies are available
+ bin/verchew
+
+# PROJECT DEPENDENCIES ########################################################
+
+VIRTUAL_ENV ?= .venv
+DEPENDENCIES := $(VIRTUAL_ENV)/.poetry-$(shell bin/checksum pyproject.toml poetry.lock)
+
+.PHONY: install
+install: $(DEPENDENCIES) .cache
+
+$(DEPENDENCIES): poetry.lock
+ @ rm -rf $(VIRTUAL_ENV)/.poetry-*
+ @ poetry config virtualenvs.in-project true
+ poetry install
+ @ touch $@
+
+ifndef CI
+poetry.lock: pyproject.toml
+ poetry lock --no-update
+ @ touch $@
+endif
+
+.cache:
+ @ mkdir -p .cache
+
+# CHECKS ######################################################################
+
+.PHONY: format
+format: install
+ poetry run isort $(PACKAGE) tests
+ poetry run black $(PACKAGE) tests
+ @ echo
+
+.PHONY: check
+check: install format ## Run formaters, linters, and static analysis
+ifdef CI
+ git diff --exit-code
+endif
+ # TODO: JB - For now, remove static analysis checks.
+ # poetry run mypy $(PACKAGE) tests --config-file=.mypy.ini
+ poetry run pylint $(PACKAGE) tests --rcfile=.pylint.ini
+ poetry run pydocstyle $(PACKAGE) tests
+
+# TESTS #######################################################################
+
+RANDOM_SEED ?= $(shell date +%s)
+FAILURES := .cache/v/cache/lastfailed
+
+PYTEST_OPTIONS := --random --random-seed=$(RANDOM_SEED)
+ifndef DISABLE_COVERAGE
+PYTEST_OPTIONS += --cov=$(PACKAGE)
+endif
+PYTEST_RERUN_OPTIONS := --last-failed --exitfirst
+
+.PHONY: test
+test: test-all ## Run unit and integration tests
+
+.PHONY: test-unit
+test-unit: install
+ @ ( mv $(FAILURES) $(FAILURES).bak || true ) > /dev/null 2>&1
+ poetry run pytest $(PACKAGE) $(PYTEST_OPTIONS)
+ @ ( mv $(FAILURES).bak $(FAILURES) || true ) > /dev/null 2>&1
+ifndef DISABLE_COVERAGE
+ poetry run coveragespace update unit
+endif
+
+.PHONY: test-int
+test-int: install
+ @ if test -e $(FAILURES); then poetry run pytest tests $(PYTEST_RERUN_OPTIONS); fi
+ @ rm -rf $(FAILURES)
+ poetry run pytest tests $(PYTEST_OPTIONS)
+ifndef DISABLE_COVERAGE
+ poetry run coveragespace update integration
+endif
+
+.PHONY: test-all
+test-all: install
+ @ if test -e $(FAILURES); then poetry run pytest $(PACKAGE) tests $(PYTEST_RERUN_OPTIONS); fi
+ @ rm -rf $(FAILURES)
+ poetry run pytest $(PACKAGE) tests $(PYTEST_OPTIONS)
+ifndef DISABLE_COVERAGE
+ poetry run coveragespace update overall
+endif
+
+.PHONY: read-coverage
+read-coverage:
+ bin/open htmlcov/index.html
+
+# DOCUMENTATION ###############################################################
+
+MKDOCS_INDEX := site/index.html
+
+.PHONY: docs
+docs: mkdocs uml ## Generate documentation and UML
+
+.PHONY: mkdocs
+mkdocs: install $(MKDOCS_INDEX)
+$(MKDOCS_INDEX): docs/requirements.txt mkdocs.yml docs/*.md
+ @ mkdir -p docs/about
+ @ cd docs && ln -sf ../README.md index.md
+ @ cd docs/about && ln -sf ../../CHANGELOG.md changelog.md
+ @ cd docs/about && ln -sf ../../CONTRIBUTING.md contributing.md
+ @ cd docs/about && ln -sf ../../LICENSE license.md
+ @ cd docs/about && ln -sf ../../ENDPOINTS.md endpoints.md
+ poetry run mkdocs build --clean --strict
+
+docs/requirements.txt: poetry.lock
+ @ poetry export --dev --without-hashes | grep mkdocs > $@
+ @ poetry export --dev --without-hashes | grep pygments >> $@
+
+.PHONY: uml
+uml: install docs/*.png
+docs/*.png: $(MODULES)
+ poetry run pyreverse $(PACKAGE) -p $(PACKAGE) -a 1 -f ALL -o png --ignore tests
+ - mv -f classes_$(PACKAGE).png docs/classes.png
+ - mv -f packages_$(PACKAGE).png docs/packages.png
+
+.PHONY: mkdocs-serve
+mkdocs-serve: mkdocs
+ eval "sleep 3; bin/open http://127.0.0.1:8000" &
+ poetry run mkdocs serve
+
+# BUILD #######################################################################
+
+DIST_FILES := dist/*.tar.gz dist/*.whl
+EXE_FILES := dist/$(PACKAGE).*
+
+.PHONY: dist
+dist: install $(DIST_FILES)
+$(DIST_FILES): $(MODULES) pyproject.toml
+ rm -f $(DIST_FILES)
+ poetry build
+
+.PHONY: exe
+exe: install $(EXE_FILES)
+$(EXE_FILES): $(MODULES) $(PACKAGE).spec
+ # For framework/shared support: https://github.com/yyuu/pyenv/wiki
+ poetry run pyinstaller $(PACKAGE).spec --noconfirm --clean
+
+$(PACKAGE).spec:
+ poetry run pyi-makespec $(PACKAGE)/__main__.py --onefile --windowed --name=$(PACKAGE)
+
+# RELEASE #####################################################################
+
+.PHONY: upload
+upload: dist ## Upload the current version to PyPI
+ git diff --name-only --exit-code
+ poetry publish
+ bin/open https://pypi.org/project/$(PACKAGE)
+
+# CLEANUP #####################################################################
+
+.PHONY: clean
+clean: .clean-build .clean-docs .clean-test .clean-install ## Delete all generated and temporary files
+
+.PHONY: clean-all
+clean-all: clean
+ rm -rf $(VIRTUAL_ENV)
+
+.PHONY: .clean-install
+.clean-install:
+ find $(PACKAGE) tests -name '__pycache__' -delete
+ rm -rf *.egg-info
+
+.PHONY: .clean-test
+.clean-test:
+ rm -rf .cache .pytest .coverage htmlcov
+
+.PHONY: .clean-docs
+.clean-docs:
+ rm -rf docs/*.png site
+
+.PHONY: .clean-build
+.clean-build:
+ rm -rf *.spec dist build
+
+# HELP ########################################################################
+
+.PHONY: help
+help: all
+ @ grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+.DEFAULT_GOAL := help
diff --git a/README.md b/README.md
index 98d1db5..79046b8 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,109 @@
-# pydefipulsedata
-Unofficial python SDK for defi pulse data
+# Overview
+
+An unofficial Python SDK for the [DeFi Pulse Data](https://docs.defipulse.com/) project and
+each of its partner service providers. This project provides a lightweight Python
+client for each service provider.
+
+Currently, the DeFi Pulse Data service providers include:
+
+- [DeFi Pulse](https://defipulse.com/)
+- [ETH Gas Station](https://ethgasstation.info/)
+- [DEX.AG](https://dex.ag/)
+- [Rek.to](https://app.rek.to/)
+- [Pools.fyi](https://pools.fyi/#/)
+
+The goals of this package are to empower Python programmers to make use of DeFi Pulse Data services,
+to enrich the broader DeFi developer ecosystem, and to reduce overall developer effort by providing
+a packaged developer SDK so that developers do not need to reinvent the wheel for each project they make.
+
+This project bears no official relationship to the DeFi Pulse Data project, or the
+[Concourse Open Community](https://concourseopen.com/) project.
+
+# Setup
+
+## Requirements
+
+* Python 3.7+
+
+## Installation
+
+Install it directly into an activated virtual environment:
+
+```text
+$ pip install defipulsedata
+```
+
+or add it to your [Poetry](https://poetry.eustace.io/) project:
+
+```text
+$ poetry add defipulsedata
+```
+
+# Usage
+
+After installation, the package can imported.
+
+Each module below corresponds to a single, logical data provider service defined in
+the [DeFi Pulse Data documentation](https://docs.defipulse.com/).
+
+```python
+from defipulsedata import RekTo, EthGasStation, DefiPulse, DexAg, PoolsFyi
+
+key='REPLACE-WITH-YOUR-KEY'
+
+# Example requests for each client.
+
+# Rek.to
+rekto = RekTo(api_key=key)
+rekto.get_events()
+
+# DeFi Pulse
+dp = DefiPulse(api_key=key)
+dp.get_projects()
+
+# ETH Gas Station
+egs = EthGasStation(api_key=key)
+egs.get_gas_price()
+
+# DEX.AG
+dexag = DexAg(api_key=key)
+dexag.get_markets()
+
+# Pools.Fyi
+pools = PoolsFyi(api_key=key)
+pools.get_exchanges()
+```
+
+# Contributing and Filing Issues
+
+Details for local development dependencies and useful Make targets can be found in `contributing.md`
+
+Contributions, suggestions, bug reports, are welcome and encouraged.
+
+If you have a bug or issue, please file a GitHub issue on the project describing the expected behavior and the actual behavior, with steps to reproduce the issue.
+
+If you have a feature request, please file a GitHub issue on the project describing the feature you want, and why you want it.
+
+# License
+
+**The MIT License (MIT)**
+
+Copyright © 2021, James Boyle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.todo.md b/README.todo.md
new file mode 100644
index 0000000..df6b71a
--- /dev/null
+++ b/README.todo.md
@@ -0,0 +1,12 @@
+Add this into the readme someday.
+
+# Ignore / TODOs
+
+This project was generated with [cookiecutter](https://github.com/audreyr/cookiecutter) using [jacebrowning/template-python](https://github.com/jacebrowning/template-python).
+
+[](https://travis-ci.com/jhhb/pydefipulsedata)
+[](https://ci.appveyor.com/project/jhhb/pydefipulsedata)
+[](https://coveralls.io/r/jhhb/pydefipulsedata)
+[](https://scrutinizer-ci.com/g/jhhb/pydefipulsedata)
+[](https://pypi.org/project/defipulsedata)
+[](https://pypi.org/project/defipulsedata)
\ No newline at end of file
diff --git a/bin/checksum b/bin/checksum
new file mode 100755
index 0000000..f38bcd6
--- /dev/null
+++ b/bin/checksum
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import hashlib
+import sys
+
+
+def run(paths):
+ sha = hashlib.sha1()
+
+ for path in paths:
+ try:
+ with open(path, 'rb') as f:
+ for chunk in iter(lambda: f.read(4096), b''):
+ sha.update(chunk)
+ except IOError:
+ sha.update(path.encode())
+
+ print(sha.hexdigest())
+
+
+if __name__ == '__main__':
+ run(sys.argv[1:])
diff --git a/bin/open b/bin/open
new file mode 100755
index 0000000..f7ae38a
--- /dev/null
+++ b/bin/open
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+
+COMMANDS = {
+ 'linux': "open",
+ 'win32': "cmd /c start",
+ 'cygwin': "cygstart",
+ 'darwin': "open",
+}
+
+
+def run(path):
+ command = COMMANDS.get(sys.platform, "open")
+ os.system(command + ' ' + path)
+
+
+if __name__ == '__main__':
+ run(sys.argv[-1])
diff --git a/bin/update b/bin/update
new file mode 100755
index 0000000..1204d5d
--- /dev/null
+++ b/bin/update
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import importlib
+import tempfile
+import shutil
+import subprocess
+import sys
+
+CWD = os.getcwd()
+TMP = tempfile.gettempdir()
+CONFIG = {
+ "full_name": "James Boyle",
+ "email": "pydefipulsedata@protonmail.com",
+ "github_username": "jhhb",
+ "github_repo": "pydefipulsedata",
+ "default_branch": "master",
+ "project_name": "defipulsedata",
+ "package_name": "defipulsedata",
+ "project_short_description": "Unofficial SDK for DeFi Pulse Data",
+ "python_major_version": 3,
+ "python_minor_version": 7,
+}
+
+
+def install(package='cookiecutter'):
+ try:
+ importlib.import_module(package)
+ except ImportError:
+ print("Installing cookiecutter")
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])
+
+
+def run():
+ print("Generating project")
+
+ from cookiecutter.main import cookiecutter
+
+ os.chdir(TMP)
+ cookiecutter(
+ 'https://github.com/jacebrowning/template-python.git',
+ no_input=True,
+ overwrite_if_exists=True,
+ extra_context=CONFIG,
+ )
+
+
+def copy():
+ for filename in [
+ '.appveyor.yml',
+ '.coveragerc',
+ '.gitattributes',
+ '.gitignore',
+ '.isort.cfg',
+ '.mypy.ini',
+ '.pydocstyle.ini',
+ '.pylint.ini',
+ '.scrutinizer.yml',
+ '.travis.yml',
+ '.verchew.ini',
+ 'CONTRIBUTING.md',
+ 'Makefile',
+ os.path.join('bin', 'checksum'),
+ os.path.join('bin', 'open'),
+ os.path.join('bin', 'update'),
+ os.path.join('bin', 'verchew'),
+ 'pytest.ini',
+ 'scent.py',
+ ]:
+ src = os.path.join(TMP, CONFIG['project_name'], filename)
+ dst = os.path.join(CWD, filename)
+ print("Updating " + filename)
+ shutil.copy(src, dst)
+
+
+if __name__ == '__main__':
+ install()
+ run()
+ copy()
diff --git a/bin/verchew b/bin/verchew
new file mode 100755
index 0000000..9ea5eeb
--- /dev/null
+++ b/bin/verchew
@@ -0,0 +1,366 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# The MIT License (MIT)
+# Copyright © 2016, Jace Browning
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+# Source: https://github.com/jacebrowning/verchew
+# Documentation: https://verchew.readthedocs.io
+# Package: https://pypi.org/project/verchew
+
+
+from __future__ import unicode_literals
+
+import argparse
+import logging
+import os
+import re
+import sys
+from collections import OrderedDict
+from subprocess import PIPE, STDOUT, Popen
+
+
+PY2 = sys.version_info[0] == 2
+
+if PY2:
+ import ConfigParser as configparser
+ from urllib import urlretrieve
+else:
+ import configparser
+ from urllib.request import urlretrieve
+
+__version__ = '3.1.1'
+
+SCRIPT_URL = (
+ "https://raw.githubusercontent.com/jacebrowning/verchew/main/verchew/script.py"
+)
+
+CONFIG_FILENAMES = ['verchew.ini', '.verchew.ini', '.verchewrc', '.verchew']
+
+SAMPLE_CONFIG = """
+[Python]
+
+cli = python
+version = Python 3.5 || Python 3.6
+
+[Legacy Python]
+
+cli = python2
+version = Python 2.7
+
+[virtualenv]
+
+cli = virtualenv
+version = 15
+message = Only required with Python 2.
+
+[Make]
+
+cli = make
+version = GNU Make
+optional = true
+
+""".strip()
+
+STYLE = {
+ "~": "✔",
+ "?": "▴",
+ "x": "✘",
+ "#": "䷉",
+}
+
+COLOR = {
+ "~": "\033[92m", # green
+ "?": "\033[93m", # yellow
+ "x": "\033[91m", # red
+ "#": "\033[96m", # cyan
+ None: "\033[0m", # reset
+}
+
+QUIET = False
+
+log = logging.getLogger(__name__)
+
+
+def main():
+ global QUIET
+
+ args = parse_args()
+ configure_logging(args.verbose)
+ if args.quiet:
+ QUIET = True
+
+ log.debug("PWD: %s", os.getenv('PWD'))
+ log.debug("PATH: %s", os.getenv('PATH'))
+
+ if args.vendor:
+ vendor_script(args.vendor)
+ sys.exit(0)
+
+ path = find_config(args.root, generate=args.init)
+ config = parse_config(path)
+
+ if not check_dependencies(config) and args.exit_code:
+ sys.exit(1)
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(description="System dependency version checker.",)
+
+ version = "%(prog)s v" + __version__
+ parser.add_argument(
+ '--version', action='version', version=version,
+ )
+ parser.add_argument(
+ '-r', '--root', metavar='PATH', help="specify a custom project root directory"
+ )
+ parser.add_argument(
+ '--exit-code',
+ action='store_true',
+ help="return a non-zero exit code on failure",
+ )
+
+ group_logging = parser.add_mutually_exclusive_group()
+ group_logging.add_argument(
+ '-v', '--verbose', action='count', default=0, help="enable verbose logging"
+ )
+ group_logging.add_argument(
+ '-q', '--quiet', action='store_true', help="suppress all output on success"
+ )
+
+ group_commands = parser.add_argument_group('commands')
+ group_commands.add_argument(
+ '--init', action='store_true', help="generate a sample configuration file"
+ )
+
+ group_commands.add_argument(
+ '--vendor', metavar='PATH', help="download the program for offline use"
+ )
+
+ args = parser.parse_args()
+
+ return args
+
+
+def configure_logging(count=0):
+ if count == 0:
+ level = logging.WARNING
+ elif count == 1:
+ level = logging.INFO
+ else:
+ level = logging.DEBUG
+
+ logging.basicConfig(level=level, format="%(levelname)s: %(message)s")
+
+
+def vendor_script(path):
+ root = os.path.abspath(os.path.join(path, os.pardir))
+ if not os.path.isdir(root):
+ log.info("Creating directory %s", root)
+ os.makedirs(root)
+
+ log.info("Downloading %s to %s", SCRIPT_URL, path)
+ urlretrieve(SCRIPT_URL, path)
+
+ log.debug("Making %s executable", path)
+ mode = os.stat(path).st_mode
+ os.chmod(path, mode | 0o111)
+
+
+def find_config(root=None, filenames=None, generate=False):
+ root = root or os.getcwd()
+ filenames = filenames or CONFIG_FILENAMES
+
+ path = None
+ log.info("Looking for config file in: %s", root)
+ log.debug("Filename options: %s", ", ".join(filenames))
+ for filename in os.listdir(root):
+ if filename in filenames:
+ path = os.path.join(root, filename)
+ log.info("Found config file: %s", path)
+ return path
+
+ if generate:
+ path = generate_config(root, filenames)
+ return path
+
+ msg = "No config file found in: {0}".format(root)
+ raise RuntimeError(msg)
+
+
+def generate_config(root=None, filenames=None):
+ root = root or os.getcwd()
+ filenames = filenames or CONFIG_FILENAMES
+
+ path = os.path.join(root, filenames[0])
+
+ log.info("Generating sample config: %s", path)
+ with open(path, 'w') as config:
+ config.write(SAMPLE_CONFIG + '\n')
+
+ return path
+
+
+def parse_config(path):
+ data = OrderedDict() # type: ignore
+
+ log.info("Parsing config file: %s", path)
+ config = configparser.ConfigParser()
+ config.read(path)
+
+ for section in config.sections():
+ data[section] = OrderedDict()
+ for name, value in config.items(section):
+ data[section][name] = value
+
+ for name in data:
+ version = data[name].get('version') or ""
+ data[name]['version'] = version
+ data[name]['patterns'] = [v.strip() for v in version.split('||')]
+
+ return data
+
+
+def check_dependencies(config):
+ success = []
+
+ for name, settings in config.items():
+ show("Checking for {0}...".format(name), head=True)
+ output = get_version(settings['cli'], settings.get('cli_version_arg'))
+
+ for pattern in settings['patterns']:
+ if match_version(pattern, output):
+ show(_("~") + " MATCHED: {0}".format(pattern or ""))
+ success.append(_("~"))
+ break
+ else:
+ if settings.get('optional'):
+ show(_("?") + " EXPECTED (OPTIONAL): {0}".format(settings['version']))
+ success.append(_("?"))
+ else:
+ if QUIET:
+ if "not found" in output:
+ actual = "Not found"
+ else:
+ actual = output.split('\n')[0].strip('.')
+ expected = settings['version'] or ""
+ print("{0}: {1}, EXPECTED: {2}".format(name, actual, expected))
+ show(
+ _("x")
+ + " EXPECTED: {0}".format(settings['version'] or "")
+ )
+ success.append(_("x"))
+ if settings.get('message'):
+ show(_("#") + " MESSAGE: {0}".format(settings['message']))
+
+ show("Results: " + " ".join(success), head=True)
+
+ return _("x") not in success
+
+
+def get_version(program, argument=None):
+ if argument is None:
+ args = [program, '--version']
+ elif argument:
+ args = [program, argument]
+ else:
+ args = [program]
+
+ show("$ {0}".format(" ".join(args)))
+ output = call(args)
+ lines = output.splitlines()
+ show(lines[0] if lines else "")
+
+ return output
+
+
+def match_version(pattern, output):
+ if "not found" in output.split('\n')[0]:
+ return False
+
+ regex = pattern.replace('.', r'\.') + r'(\b|/)'
+
+ log.debug("Matching %s: %s", regex, output)
+ match = re.match(regex, output)
+ if match is None:
+ match = re.match(r'.*[^\d.]' + regex, output)
+
+ return bool(match)
+
+
+def call(args):
+ try:
+ process = Popen(args, stdout=PIPE, stderr=STDOUT)
+ except OSError:
+ log.debug("Command not found: %s", args[0])
+ output = "sh: command not found: {0}".format(args[0])
+ else:
+ raw = process.communicate()[0]
+ output = raw.decode('utf-8').strip()
+ log.debug("Command output: %r", output)
+
+ return output
+
+
+def show(text, start='', end='\n', head=False):
+ """Python 2 and 3 compatible version of print."""
+ if QUIET:
+ return
+
+ if head:
+ start = '\n'
+ end = '\n\n'
+
+ if log.getEffectiveLevel() < logging.WARNING:
+ log.info(text)
+ else:
+ formatted = start + text + end
+ if PY2:
+ formatted = formatted.encode('utf-8')
+ sys.stdout.write(formatted)
+ sys.stdout.flush()
+
+
+def _(word, is_tty=None, supports_utf8=None, supports_ansi=None):
+ """Format and colorize a word based on available encoding."""
+ formatted = word
+
+ if is_tty is None:
+ is_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
+ if supports_utf8 is None:
+ supports_utf8 = str(sys.stdout.encoding).lower() == 'utf-8'
+ if supports_ansi is None:
+ supports_ansi = sys.platform != 'win32' or 'ANSICON' in os.environ
+
+ style_support = supports_utf8
+ color_support = is_tty and supports_ansi
+
+ if style_support:
+ formatted = STYLE.get(word, word)
+
+ if color_support and COLOR.get(word):
+ formatted = COLOR[word] + formatted + COLOR[None]
+
+ return formatted
+
+
+if __name__ == '__main__': # pragma: no cover
+ main()
diff --git a/defipulsedata/__init__.py b/defipulsedata/__init__.py
new file mode 100644
index 0000000..34424c7
--- /dev/null
+++ b/defipulsedata/__init__.py
@@ -0,0 +1,5 @@
+from .defi_pulse import DefiPulse
+from .dex_ag import DexAg
+from .eth_gas_station import EthGasStation
+from .pools_fyi import PoolsFyi
+from .rek_to import RekTo
diff --git a/defipulsedata/defi_pulse.py b/defipulsedata/defi_pulse.py
new file mode 100644
index 0000000..cc345f6
--- /dev/null
+++ b/defipulsedata/defi_pulse.py
@@ -0,0 +1,88 @@
+import warnings
+from urllib import parse
+
+from .utils import filter_null_keys, get_request, validate_allowed_params
+
+
+class DefiPulse:
+ __API_URL_BASE = 'https://data-api.defipulse.com/api/v1/defipulse/api'
+
+ def __init__(self, *, api_key):
+ self.api_base_url = self.__API_URL_BASE
+ self.base_params = {'api-key': api_key}
+
+ def get_market_data(self):
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/MarketData?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_history(self, *, params=None):
+ allowed_params = {
+ 'project',
+ 'period',
+ 'length',
+ 'resolution',
+ 'category',
+ 'api-key',
+ }
+ function_params = params or {}
+
+ if 'period' in function_params and 'length' in function_params:
+ warnings.warn('API only supports "period" or "length" params exclusively.')
+ merged_params = {**function_params, **self.base_params}
+ validate_allowed_params(merged_params, allowed_params)
+
+ encoded_params = parse.urlencode(merged_params)
+ api_url = '{0}/GetHistory?{1}'.format(self.api_base_url, encoded_params)
+
+ return get_request(api_url)
+
+ def get_projects(self):
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/GetProjects?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_lending_tokens(self):
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/GetLendingTokens?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_lending_market_data(self):
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/LendingMarketData?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_lending_projects(self):
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/GetLendingProjects?{1}'.format(self.api_base_url, encoded_params)
+
+ return get_request(api_url)
+
+ def get_lending_history(self, *, params=None):
+ allowed_params = {
+ 'period',
+ 'length',
+ 'resolution',
+ 'format',
+ 'api-key',
+ }
+ function_params = params or {}
+ if 'period' in function_params and 'length' in function_params:
+ warnings.warn('API only supports "period" or "length" params exclusively.')
+
+ merged_params = {**function_params, **self.base_params}
+ validate_allowed_params(merged_params, allowed_params)
+
+ encoded_params = parse.urlencode(merged_params)
+ api_url = '{0}/getLendingHistory?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_rates(self, *, token, amount=None):
+ allowed_params = {'token', 'amount', 'api-key'}
+ merged_params = {'token': token, 'amount': amount, **self.base_params}
+ filtered_params = filter_null_keys(merged_params)
+ validate_allowed_params(filtered_params, allowed_params)
+
+ encoded_params = parse.urlencode(filtered_params)
+ api_url = '{0}/GetRates?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
diff --git a/defipulsedata/dex_ag.py b/defipulsedata/dex_ag.py
new file mode 100644
index 0000000..ece960e
--- /dev/null
+++ b/defipulsedata/dex_ag.py
@@ -0,0 +1,58 @@
+from urllib import parse
+
+from .utils import get_request, validate_allowed_params
+
+
+class DexAg:
+ __API_URL_BASE = 'https://data-api.defipulse.com/api/v1/dexag'
+
+ def __init__(self, *, api_key):
+ self.base_params = {'api-key': api_key}
+ self.api_base_url = self.__API_URL_BASE
+
+ def get_markets(self):
+ # https://data-api.defipulse.com/api/v1/dexag/markets
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/markets?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_token_list_full(self):
+ # https://data-api.defipulse.com/api/v1/dexag/token-list-full
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/token-list-full?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_price(self, *, fromToken, toToken, dex='all', params=None):
+ # https://data-api.defipulse.com/api/v1/dexag/price?from=ETH&to=DAI&fromAmount=1&dex=ag
+
+ required_params = {
+ 'from': fromToken,
+ 'to': toToken,
+ 'dex': dex,
+ }
+ function_params = params or {}
+ merged_params = {**function_params, **required_params, **self.base_params}
+
+ allowed_params = {
+ 'from',
+ 'to',
+ 'fromAmount',
+ 'toAmount',
+ 'dex',
+ 'discluded',
+ 'api-key',
+ }
+
+ from_amount, to_amount = merged_params.get('fromAmount'), merged_params.get(
+ 'toAmount'
+ )
+ if not (from_amount or to_amount):
+ raise ValueError("Either from_amount or to_amount must be specified.")
+ if from_amount and to_amount:
+ raise ValueError("Only one of from_amount or to_amount may be specified.")
+
+ validate_allowed_params(merged_params, allowed_params)
+
+ encoded_params = parse.urlencode(merged_params)
+ api_url = '{0}/price?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
diff --git a/defipulsedata/eth_gas_station.py b/defipulsedata/eth_gas_station.py
new file mode 100644
index 0000000..b4f0fb9
--- /dev/null
+++ b/defipulsedata/eth_gas_station.py
@@ -0,0 +1,21 @@
+from urllib import parse
+
+from .utils import get_request
+
+
+class EthGasStation:
+ __API_URL_BASE = 'https://data-api.defipulse.com/api/v1/egs/api'
+
+ def __init__(self, *, api_key):
+ self.api_base_url = self.__API_URL_BASE
+ self.base_params = {'api-key': api_key}
+
+ def get_gas_price(self):
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/ethgasAPI.json?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_prediction_table(self):
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/predictTable.json?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
diff --git a/defipulsedata/pools_fyi.py b/defipulsedata/pools_fyi.py
new file mode 100644
index 0000000..3e7ec79
--- /dev/null
+++ b/defipulsedata/pools_fyi.py
@@ -0,0 +1,84 @@
+from urllib import parse
+
+from .utils import get_request, validate_allowed_params
+
+
+class PoolsFyi:
+ __API_URL_BASE = 'https://data-api.defipulse.com/api/v1/blocklytics/pools'
+
+ def __init__(self, *, api_key):
+ self.api_base_url = self.__API_URL_BASE
+ self.base_params = {'api-key': api_key}
+
+ def get_exchanges(self, *, params=None):
+ # Example URL:
+ # https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/exchanges
+ allowed_params = {
+ 'tags',
+ 'platform',
+ 'direction',
+ 'orderBy',
+ 'offset',
+ 'limit',
+ 'api-key',
+ }
+ function_params = params or {}
+ merged_params = {**function_params, **self.base_params}
+
+ validate_allowed_params(merged_params, allowed_params)
+
+ encoded_params = parse.urlencode(merged_params)
+ api_url = '{0}/v1/exchanges?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_returns(self, *, address):
+ # Example URL for UNI-V2 ETH/GRT:
+ # https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/returns/0x2e81ec0b8b4022fac83a21b2f2b4b8f5ed744d70
+
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/v1/returns/{1}?{2}'.format(
+ self.api_base_url, address, encoded_params
+ )
+ return get_request(api_url)
+
+ def get_liquidity(self, *, address):
+ # Returns the owners of liquidity on the AMM
+ # Example URL: https://data-api.defipulse.com/api/v1/blocklytics/pools/v0/liquidity/0x2e81ec0b8b4022fac83a21b2f2b4b8f5ed744d70/owners
+
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/v0/liquidity/{1}?{2}'.format(
+ self.api_base_url, address, encoded_params
+ )
+ return get_request(api_url)
+
+ def get_exchange(self, *, address):
+ # Example URL:
+ # https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/exchange/0x2e81ec0b8b4022fac83a21b2f2b4b8f5ed744d70
+
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/v1/exchange/{1}?{2}'.format(
+ self.api_base_url, address, encoded_params
+ )
+ return get_request(api_url)
+
+ def get_trades(self, *, address, params=None):
+ allowed_params = {
+ 'platform',
+ 'direction',
+ 'orderBy',
+ 'offset',
+ 'limit',
+ 'to',
+ 'from',
+ 'api-key',
+ }
+ function_params = params or {}
+ merged_params = {**function_params, **self.base_params}
+
+ validate_allowed_params(merged_params, allowed_params)
+
+ encoded_params = parse.urlencode(merged_params)
+ api_url = '{0}/v1/trades/{1}?{2}'.format(
+ self.api_base_url, address, encoded_params
+ )
+ return get_request(api_url)
diff --git a/defipulsedata/rek_to.py b/defipulsedata/rek_to.py
new file mode 100644
index 0000000..4207747
--- /dev/null
+++ b/defipulsedata/rek_to.py
@@ -0,0 +1,29 @@
+from urllib import parse
+
+from .utils import get_request
+
+
+class RekTo:
+ __API_URL_BASE = 'https://data-api.defipulse.com/api/v1/rekto/api'
+
+ def __init__(self, *, api_key):
+ self.api_base_url = self.__API_URL_BASE
+ self.base_params = {'api-key': api_key}
+
+ def get_events(self):
+ # https://data-api.defipulse.com/api/v1/rekto/api/events
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/events?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_top_10(self):
+ # https://data-api.defipulse.com/api/v1/rekto/api/top10
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/top10?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
+
+ def get_total_damage(self):
+ # https://data-api.defipulse.com/api/v1/rekto/api/total-damage
+ encoded_params = parse.urlencode(self.base_params)
+ api_url = '{0}/total-damage?{1}'.format(self.api_base_url, encoded_params)
+ return get_request(api_url)
diff --git a/defipulsedata/utils.py b/defipulsedata/utils.py
new file mode 100644
index 0000000..34e2012
--- /dev/null
+++ b/defipulsedata/utils.py
@@ -0,0 +1,30 @@
+import json
+
+import requests
+
+
+# TODO: JB - Revisit this as we learn more about error handling.
+def get_request(url, **kwargs):
+ timeout = kwargs.get('timeout', 10)
+
+ try:
+ response = requests.get(url, timeout=timeout)
+ response.raise_for_status()
+ content = json.loads(response.content.decode('utf-8'))
+ return content
+ except (json.decoder.JSONDecodeError, requests.HTTPError) as e:
+ raise e
+ except Exception as e:
+ message = "Unexpected exception type: {type}".format(type=e.__class__.__name__)
+ raise Exception(message) from e
+
+
+def validate_allowed_params(actual_params, allowed_params):
+ for k in actual_params:
+ if k not in allowed_params:
+ message = "Received unexpected param: {0}".format(k)
+ raise ValueError(message)
+
+
+def filter_null_keys(_dict):
+ return {k: v for k, v in _dict.items() if v is not None}
diff --git a/docs/about/changelog.md b/docs/about/changelog.md
new file mode 120000
index 0000000..699cc9e
--- /dev/null
+++ b/docs/about/changelog.md
@@ -0,0 +1 @@
+../../CHANGELOG.md
\ No newline at end of file
diff --git a/docs/about/contributing.md b/docs/about/contributing.md
new file mode 120000
index 0000000..f939e75
--- /dev/null
+++ b/docs/about/contributing.md
@@ -0,0 +1 @@
+../../CONTRIBUTING.md
\ No newline at end of file
diff --git a/docs/about/endpoints.md b/docs/about/endpoints.md
new file mode 120000
index 0000000..bdbaa0c
--- /dev/null
+++ b/docs/about/endpoints.md
@@ -0,0 +1 @@
+../../ENDPOINTS.md
\ No newline at end of file
diff --git a/docs/about/license.md b/docs/about/license.md
new file mode 120000
index 0000000..30cff74
--- /dev/null
+++ b/docs/about/license.md
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..02ab62f
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,2 @@
+mkdocs==1.0.4; (python_full_version >= "2.7.9" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
+pygments==2.8.1; python_version >= "3.5"
diff --git a/endpoints.md b/endpoints.md
new file mode 100644
index 0000000..30f3920
--- /dev/null
+++ b/endpoints.md
@@ -0,0 +1,43 @@
+## Summary
+This page documents any quirks in the APIs that are worth documenting and ideally
+fixing or clarifying in either the API docs, or the API implementations.
+
+Any quirks that affect the runtime behavior and expectations could be
+good opportunities for adding in warning messages in the client.
+
+## [ETH Gas Station](https://web.archive.org/web/20210602120344/https://docs.defipulse.com/api-docs-by-provider/egs)
+
+## [Rek.to](https://web.archive.org/web/20210602120354/https://docs.defipulse.com/api-docs-by-provider/rek.to)
+
+- [`/events`](https://web.archive.org/web/20210602120843/https://docs.defipulse.com/api-docs-by-provider/rek.to/events)
+ - It looks like `minSize` and `symbol` params do not work, however, we can go to app.rek.to and clearly see these query params working in a network request there.
+
+- [`/top10`](https://web.archive.org/web/20210602120845/https://docs.defipulse.com/api-docs-by-provider/rek.to/untitled)
+ - It looks like `minSize` and `symbol` params do not work.
+
+- [docs](https://web.archive.org/web/20210602120843/https://docs.defipulse.com/api-docs-by-provider/rek.to/events)
+ - docs point to `defiupulse` instead of `defipulse`.
+
+## [Pools.fyi](https://web.archive.org/web/20210602120320/https://docs.defipulse.com/api-docs-by-provider/pools.fyi)
+
+- [`/returns`](https://web.archive.org/web/20210602121958/https://docs.defipulse.com/api-docs-by-provider/pools.fyi/returns-by-exchange)
+ - This endpoint returns ~30 days of returns data for a particular *liquidity pool* address over time,
+ for example, UNI-V2 ETH/GRT.
+ - The endpoint will *not* return data across AMMs for an individual token address, like GRT or ETH.
+ - The docs' "Request" section could be updated to include `address` as a path param.
+
+- [`/liquidity`](https://web.archive.org/web/20210602124621/https://docs.defipulse.com/api-docs-by-provider/pools.fyi/pool-liquidity)
+ - The use of `v0` in the URL is not a typo, even though this is the only Pools.fyi endpoint that uses this `v0` path.
+
+- [`/exchange`](https://web.archive.org/web/20210602124630/https://docs.defipulse.com/api-docs-by-provider/pools.fyi/single-exchange)
+ - The docs currently point to an invalid base URL; the same base URL as the other endpoints is the true one.
+
+## [DeFi Pulse](https://web.archive.org/web/20210602120334/https://docs.defipulse.com/api-docs-by-provider/defi-pulse-data)
+
+## [DEX.AG](https://web.archive.org/web/20210602120338/https://docs.defipulse.com/api-docs-by-provider/dex.ag)
+
+- [`/price`](https://web.archive.org/web/20210602120306/https://docs.defipulse.com/api-docs-by-provider/dex.ag/untitled)
+ - `fromAmount` and `toAmount` are exclusive options.
+ - Both of `fromToken` and `toToken` are required.
+ - The API docs specify that `dex` is optional, but it appears to be required in order to work.
+ - `discluded` works but seems unable to exclude some DEXes -- specifically, AG appears at the end of the response array, and cannot be excluded when tried with `?disclude=ag`.
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..44608ce
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,19 @@
+site_name: defipulsedata
+site_description: Unofficial SDK for DeFi Pulse Data
+site_author: James Boyle
+
+repo_url: https://github.com/jhhb/pydefipulsedata
+edit_uri: https://github.com/jhhb/pydefipulsedata/edit/master/docs
+
+theme: readthedocs
+
+markdown_extensions:
+ - codehilite
+
+nav:
+ - Home: index.md
+ - About:
+ - Release Notes: about/changelog.md
+ - Contributing: about/contributing.md
+ - License: about/license.md
+ - Endpoints: about/endpoints.md
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..3b42e24
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1461 @@
+[[package]]
+name = "altgraph"
+version = "0.17"
+description = "Python graph (network) package"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[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 = "appnope"
+version = "0.1.2"
+description = "Disable App Nap on macOS >= 10.9"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "astroid"
+version = "2.4.2"
+description = "An abstract syntax tree for Python with inference support."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+lazy-object-proxy = ">=1.4.0,<1.5.0"
+six = ">=1.12,<2.0"
+typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""}
+wrapt = ">=1.11,<2.0"
+
+[[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 = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[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"
+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 = "certifi"
+version = "2020.12.5"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "chardet"
+version = "4.0.0"
+description = "Universal encoding detector for Python 2 and 3"
+category = "main"
+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 = "main"
+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.extras]
+toml = ["toml"]
+
+[[package]]
+name = "coveragespace"
+version = "4.1"
+description = "A place to track your code coverage metrics."
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+colorama = ">=0.4"
+coverage = ">=4.0"
+docopt = ">=0.6"
+minilog = ">=2.0,<3.0"
+requests = ">=2.0,<3.0"
+
+[[package]]
+name = "decorator"
+version = "4.4.2"
+description = "Decorators for Humans"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*"
+
+[[package]]
+name = "docopt"
+version = "0.6.2"
+description = "Pythonic argument parser, that will make you smile"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "freezegun"
+version = "1.1.0"
+description = "Let your Python tests travel through time"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+python-dateutil = ">=2.7"
+
+[[package]]
+name = "future"
+version = "0.18.2"
+description = "Clean single-source support for Python 3 and 2"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "idna"
+version = "2.10"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "importlib-metadata"
+version = "3.10.0"
+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 (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "ipython"
+version = "7.22.0"
+description = "IPython: Productive Interactive Computing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+appnope = {version = "*", markers = "sys_platform == \"darwin\""}
+backcall = "*"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+decorator = "*"
+jedi = ">=0.16"
+pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
+pickleshare = "*"
+prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
+pygments = "*"
+traitlets = ">=4.2"
+
+[package.extras]
+all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.16)", "pygments", "qtconsole", "requests", "testpath"]
+doc = ["Sphinx (>=1.3)"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["notebook", "ipywidgets"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.16)"]
+
+[[package]]
+name = "ipython-genutils"
+version = "0.2.0"
+description = "Vestigial utilities from IPython"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "isort"
+version = "5.5.1"
+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 = "jedi"
+version = "0.18.0"
+description = "An autocompletion tool for Python that can be used for text editors."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+parso = ">=0.8.0,<0.9.0"
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
+
+[[package]]
+name = "jinja2"
+version = "2.11.3"
+description = "A very fast and expressive template engine."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+MarkupSafe = ">=0.23"
+
+[package.extras]
+i18n = ["Babel (>=0.8)"]
+
+[[package]]
+name = "lazy-object-proxy"
+version = "1.4.3"
+description = "A fast and thorough lazy object proxy."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "livereload"
+version = "2.6.3"
+description = "Python LiveReload is an awesome tool for web developers"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
+tornado = {version = "*", markers = "python_version > \"2.7\""}
+
+[[package]]
+name = "macfsevents"
+version = "0.8.1"
+description = "Thread-based interface to file system observation primitives."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "macholib"
+version = "1.14"
+description = "Mach-O header analysis and editing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+altgraph = ">=0.15"
+
+[[package]]
+name = "markdown"
+version = "3.3.4"
+description = "Python implementation of Markdown."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markupsafe"
+version = "1.1.1"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "minilog"
+version = "2.0"
+description = "Minimalistic wrapper for Python logging."
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[[package]]
+name = "mkdocs"
+version = "1.0.4"
+description = "Project documentation with Markdown."
+category = "dev"
+optional = false
+python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+
+[package.dependencies]
+click = ">=3.3"
+Jinja2 = ">=2.7.1"
+livereload = ">=2.5.1"
+Markdown = ">=2.3.1"
+PyYAML = ">=3.10"
+tornado = ">=5.0"
+
+[[package]]
+name = "more-itertools"
+version = "8.7.0"
+description = "More routines for operating on iterables, beyond itertools"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "mypy"
+version = "0.812"
+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 = "nose"
+version = "1.3.7"
+description = "nose extends unittest to make testing easier"
+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 = "parso"
+version = "0.8.1"
+description = "A Python Parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["docopt", "pytest (<6.0.0)"]
+
+[[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 = "pefile"
+version = "2019.4.18"
+description = "Python PE parsing module"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+future = "*"
+
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "pickleshare"
+version = "0.7.5"
+description = "Tiny 'shelve'-like database with concurrency support"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[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 = "prompt-toolkit"
+version = "3.0.18"
+description = "Library for building powerful interactive command lines in Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[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 = "pydocstyle"
+version = "6.0.0"
+description = "Python docstring style checker"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+snowballstemmer = "*"
+
+[[package]]
+name = "pygments"
+version = "2.8.1"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "pyinstaller"
+version = "4.2"
+description = "PyInstaller bundles a Python application and all its dependencies into a single package."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+altgraph = "*"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
+pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""}
+pyinstaller-hooks-contrib = ">=2020.6"
+pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+encryption = ["tinyaes (>=1.0.0)"]
+hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
+
+[[package]]
+name = "pyinstaller-hooks-contrib"
+version = "2021.1"
+description = "Community maintained hooks for PyInstaller"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pylint"
+version = "2.6.2"
+description = "python code static checker"
+category = "dev"
+optional = false
+python-versions = ">=3.5.*"
+
+[package.dependencies]
+astroid = ">=2.4.0,<2.5"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+isort = ">=4.2.5,<6"
+mccabe = ">=0.6,<0.7"
+toml = ">=0.7.1"
+
+[[package]]
+name = "pync"
+version = "2.0.3"
+description = "Python Wrapper for Mac OS 10.10 Notification Center"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+python-dateutil = ">=2.0"
+
+[[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 = "pytest"
+version = "5.4.3"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=17.4.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+more-itertools = ">=4.0.0"
+packaging = "*"
+pluggy = ">=0.12,<1.0"
+py = ">=1.5.0"
+wcwidth = "*"
+
+[package.extras]
+checkqa-mypy = ["mypy (==v0.761)"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[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 = "pytest-describe"
+version = "0.12.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = "*"
+develop = false
+
+[package.dependencies]
+pytest = ">=2.6.0"
+
+[package.source]
+type = "git"
+url = "https://github.com/pytest-dev/pytest-describe"
+reference = "453aa9045b265e313f356f1492d8991c02a6aea6"
+resolved_reference = "453aa9045b265e313f356f1492d8991c02a6aea6"
+
+[[package]]
+name = "pytest-expecter"
+version = "2.2"
+description = "Better testing with expecter and pytest."
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[[package]]
+name = "pytest-random"
+version = "0.02"
+description = "py.test plugin to randomize tests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pytest = ">=2.2.3"
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.1"
+description = "Extensions to the standard Python datetime module"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-termstyle"
+version = "0.1.10"
+description = "console colouring for python"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[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 = "main"
+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 = "responses"
+version = "0.13.3"
+description = "A utility library for mocking out the `requests` Python library."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+requests = ">=2.0"
+six = "*"
+urllib3 = ">=1.25.10"
+
+[package.extras]
+tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"]
+
+[[package]]
+name = "six"
+version = "1.15.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "sniffer"
+version = "0.4.1"
+description = "An automatic test runner. Supports nose out of the box."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+colorama = "*"
+nose = "*"
+python-termstyle = "*"
+
+[package.extras]
+growl = ["gntp (==0.7)"]
+libnotify = ["py-notify (==0.3.1)"]
+linux = ["pyinotify (==0.9.0)"]
+osx = ["MacFSEvents (==0.2.8)"]
+
+[[package]]
+name = "snowballstemmer"
+version = "2.1.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[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 = "tornado"
+version = "6.1"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+category = "dev"
+optional = false
+python-versions = ">= 3.5"
+
+[[package]]
+name = "traitlets"
+version = "5.0.5"
+description = "Traitlets Python configuration system"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+ipython-genutils = "*"
+
+[package.extras]
+test = ["pytest"]
+
+[[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 = "main"
+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 = "wcwidth"
+version = "0.2.5"
+description = "Measures the displayed width of unicode strings in a terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[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.7"
+content-hash = "a1ed6be4346e47479b4706d4a87b96947aa932f434dc97bb4bb737bc2de24666"
+
+[metadata.files]
+altgraph = [
+ {file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"},
+ {file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"},
+]
+appdirs = [
+ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+appnope = [
+ {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
+ {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
+]
+astroid = [
+ {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"},
+ {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"},
+]
+atomicwrites = [
+ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+ {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
+ {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
+]
+backcall = [
+ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
+ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
+]
+black = [
+ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
+]
+certifi = [
+ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
+ {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+]
+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-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"},
+ {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"},
+ {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"},
+ {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"},
+]
+coveragespace = [
+ {file = "coveragespace-4.1-py3-none-any.whl", hash = "sha256:a59fa4227166406f74c0fd89ad871b6d699d35de5468eeca96bf556838af0f0c"},
+ {file = "coveragespace-4.1.tar.gz", hash = "sha256:6dcdee802be5cdaa9820538203ad0e182a1ea56c679cdf65b36d4b85937d2e38"},
+]
+decorator = [
+ {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
+ {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"},
+]
+docopt = [
+ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
+]
+freezegun = [
+ {file = "freezegun-1.1.0-py2.py3-none-any.whl", hash = "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712"},
+ {file = "freezegun-1.1.0.tar.gz", hash = "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3"},
+]
+future = [
+ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
+]
+idna = [
+ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
+ {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+]
+importlib-metadata = [
+ {file = "importlib_metadata-3.10.0-py3-none-any.whl", hash = "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe"},
+ {file = "importlib_metadata-3.10.0.tar.gz", hash = "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a"},
+]
+ipython = [
+ {file = "ipython-7.22.0-py3-none-any.whl", hash = "sha256:c0ce02dfaa5f854809ab7413c601c4543846d9da81010258ecdab299b542d199"},
+ {file = "ipython-7.22.0.tar.gz", hash = "sha256:9c900332d4c5a6de534b4befeeb7de44ad0cc42e8327fa41b7685abde58cec74"},
+]
+ipython-genutils = [
+ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
+ {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"},
+]
+isort = [
+ {file = "isort-5.5.1-py3-none-any.whl", hash = "sha256:a200d47b7ee8b7f7d0a9646650160c4a51b6a91a9413fd31b1da2c4de789f5d3"},
+ {file = "isort-5.5.1.tar.gz", hash = "sha256:92533892058de0306e51c88f22ece002a209dc8e80288aa3cec6d443060d584f"},
+]
+jedi = [
+ {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"},
+ {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
+]
+jinja2 = [
+ {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
+ {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
+]
+lazy-object-proxy = [
+ {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
+ {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
+ {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"},
+ {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"},
+ {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"},
+ {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"},
+ {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"},
+ {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"},
+ {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"},
+ {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"},
+ {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"},
+ {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"},
+ {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"},
+ {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"},
+ {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"},
+ {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"},
+ {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"},
+ {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"},
+ {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"},
+ {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
+ {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"},
+]
+livereload = [
+ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},
+]
+macfsevents = [
+ {file = "MacFSEvents-0.8.1.tar.gz", hash = "sha256:1324b66b356051de662ba87d84f73ada062acd42b047ed1246e60a449f833e10"},
+]
+macholib = [
+ {file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"},
+ {file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"},
+]
+markdown = [
+ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
+ {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
+]
+markupsafe = [
+ {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
+ {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
+ {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
+ {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
+ {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
+ {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
+ {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
+ {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
+ {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
+ {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
+ {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
+ {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
+ {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
+ {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
+ {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
+ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
+ {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
+ {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
+ {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
+ {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"},
+ {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"},
+ {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
+]
+mccabe = [
+ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+minilog = [
+ {file = "minilog-2.0-py3-none-any.whl", hash = "sha256:891ad346bdd63aee4c210faa688497f7ba412cf52a54fb6cba6bf511a34a5138"},
+ {file = "minilog-2.0.tar.gz", hash = "sha256:58499302bca86cf507eb3c3dfa3853de9389bfde275ba5155bdc3b4551175918"},
+]
+mkdocs = [
+ {file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"},
+ {file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"},
+]
+more-itertools = [
+ {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"},
+ {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"},
+]
+mypy = [
+ {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"},
+ {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"},
+ {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"},
+ {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"},
+ {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"},
+ {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"},
+ {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"},
+ {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"},
+ {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"},
+ {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"},
+ {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"},
+ {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"},
+ {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"},
+ {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"},
+ {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"},
+ {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"},
+ {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"},
+ {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"},
+ {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"},
+ {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"},
+ {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"},
+ {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"},
+]
+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"},
+]
+nose = [
+ {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"},
+ {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"},
+ {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"},
+]
+packaging = [
+ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
+ {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
+]
+parso = [
+ {file = "parso-0.8.1-py2.py3-none-any.whl", hash = "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410"},
+ {file = "parso-0.8.1.tar.gz", hash = "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e"},
+]
+pathspec = [
+ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
+ {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
+]
+pefile = [
+ {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"},
+]
+pexpect = [
+ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
+ {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
+]
+pickleshare = [
+ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
+ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
+]
+pluggy = [
+ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+prompt-toolkit = [
+ {file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"},
+ {file = "prompt_toolkit-3.0.18.tar.gz", hash = "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"},
+]
+ptyprocess = [
+ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+py = [
+ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+ {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
+pydocstyle = [
+ {file = "pydocstyle-6.0.0-py3-none-any.whl", hash = "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d"},
+ {file = "pydocstyle-6.0.0.tar.gz", hash = "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f"},
+]
+pygments = [
+ {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
+ {file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},
+]
+pyinstaller = [
+ {file = "pyinstaller-4.2.tar.gz", hash = "sha256:f5c0eeb2aa663cce9a5404292c0195011fa500a6501c873a466b2e8cad3c950c"},
+]
+pyinstaller-hooks-contrib = [
+ {file = "pyinstaller-hooks-contrib-2021.1.tar.gz", hash = "sha256:892310e6363655838485ee748bf1c5e5cade7963686d9af8650ee218a3e0b031"},
+ {file = "pyinstaller_hooks_contrib-2021.1-py2.py3-none-any.whl", hash = "sha256:27558072021857d89524c42136feaa2ffe4f003f1bdf0278f9b24f6902c1759c"},
+]
+pylint = [
+ {file = "pylint-2.6.2-py3-none-any.whl", hash = "sha256:e71c2e9614a4f06e36498f310027942b0f4f2fde20aebb01655b31edc63b9eaf"},
+ {file = "pylint-2.6.2.tar.gz", hash = "sha256:718b74786ea7ed07aa0c58bf572154d4679f960d26e9641cc1de204a30b87fc9"},
+]
+pync = [
+ {file = "pync-2.0.3.tar.gz", hash = "sha256:38b9e61735a3161f9211a5773c5f5ea698f36af4ff7f77fa03e8d1ff0caa117f"},
+]
+pyparsing = [
+ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+pytest = [
+ {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
+ {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
+]
+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"},
+]
+pytest-describe = []
+pytest-expecter = [
+ {file = "pytest-expecter-2.2.tar.gz", hash = "sha256:33c5b05008cad9b95ba0deb0a5bac6a51c68ae94f09de74a19cd1384a04c85d6"},
+ {file = "pytest_expecter-2.2-py3-none-any.whl", hash = "sha256:15dd0485c23fd03b7a9c0f6819f0cf65c0defd0ce9796725ae51e79e56b279ab"},
+]
+pytest-random = [
+ {file = "pytest-random-0.02.tar.gz", hash = "sha256:92f25db8c5d9ffc20d90b51997b914372d6955cb9cf1f6ead45b90514fc0eddd"},
+]
+python-dateutil = [
+ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
+ {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
+]
+python-termstyle = [
+ {file = "python-termstyle-0.1.10.tar.gz", hash = "sha256:f42a6bb16fbfc5e2c66d553e7ad46524ea833872f75ee5d827c15115fafc94e2"},
+ {file = "python-termstyle-0.1.10.tgz", hash = "sha256:6faf42ba42f2826c38cf70dacb3ac51f248a418e48afc0e36593df11cf3ab1d2"},
+]
+pywin32-ctypes = [
+ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
+ {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
+]
+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-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
+ {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
+ {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-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
+ {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
+ {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-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
+ {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
+ {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-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
+ {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
+ {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"},
+]
+responses = [
+ {file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"},
+ {file = "responses-0.13.3.tar.gz", hash = "sha256:18a5b88eb24143adbf2b4100f328a2f5bfa72fbdacf12d97d41f07c26c45553d"},
+]
+six = [
+ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
+ {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
+]
+sniffer = [
+ {file = "sniffer-0.4.1-py2.py3-none-any.whl", hash = "sha256:f120843fe152d0e380402fc11313b151e2044c47fdd36895de2efedc8624dbb8"},
+ {file = "sniffer-0.4.1.tar.gz", hash = "sha256:b37665053fb83d7790bf9e51d616c11970863d14b5ea5a51155a4e95759d1529"},
+]
+snowballstemmer = [
+ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
+ {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
+]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tornado = [
+ {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
+ {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
+ {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"},
+ {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"},
+ {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"},
+ {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"},
+ {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"},
+ {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"},
+ {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"},
+ {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"},
+ {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"},
+ {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"},
+ {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"},
+ {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"},
+ {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"},
+ {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"},
+ {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"},
+ {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"},
+ {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"},
+ {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"},
+ {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"},
+ {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"},
+ {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"},
+ {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"},
+ {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"},
+ {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"},
+ {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"},
+ {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"},
+ {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"},
+ {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"},
+ {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"},
+ {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"},
+ {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"},
+ {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"},
+ {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"},
+ {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"},
+ {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"},
+ {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"},
+ {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"},
+ {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
+ {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
+]
+traitlets = [
+ {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"},
+ {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"},
+]
+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"},
+]
+wcwidth = [
+ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
+ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
+]
+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..a16ff5e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,78 @@
+[tool.poetry]
+
+name = "defipulsedata"
+version = "0.0.0-pre"
+description = "Unofficial SDK for DeFi Pulse Data"
+
+license = "MIT"
+
+authors = ["James Boyle "]
+
+readme = "README.md"
+
+homepage = "https://pypi.org/project/defipulsedata"
+documentation = "https://defipulsedata.readthedocs.io"
+repository = "https://github.com/jhhb/pydefipulsedata"
+
+keywords = [
+ "DeFi",
+ "Decentralized Finance",
+ "SDK",
+ "Client"
+]
+
+[tool.poetry.dependencies]
+
+python = "^3.7"
+
+# TODO: Remove these and add your library's requirements
+click = "^7.0"
+minilog = "^2.0"
+responses = "^0.13.3"
+
+[tool.poetry.dev-dependencies]
+
+# Formatters
+black = "=20.8b1"
+isort = "=5.5.1"
+
+# Linters
+mypy = "*"
+pydocstyle = "*"
+pylint = "~2.6.0"
+
+# Testing
+pytest = "^5.3.2"
+pytest-cov = "*"
+pytest-describe = { git = "https://github.com/pytest-dev/pytest-describe", rev = "453aa9045b265e313f356f1492d8991c02a6aea6" } # use 2.0 when released
+pytest-expecter = "^2.1"
+pytest-random = "*"
+freezegun = "*"
+
+# Reports
+coveragespace = "^4.0"
+
+# Documentation
+mkdocs = "~1.0"
+pygments = "^2.5.2"
+
+# Tooling
+pyinstaller = "*"
+sniffer = "*"
+MacFSEvents = { version = "*", platform = "darwin" }
+pync = { version = "*", platform = "darwin" }
+ipython = "^7.12.0"
+
+[tool.poetry.scripts]
+
+defipulsedata = "defipulsedata.cli:main"
+
+[tool.black]
+
+target-version = ["py36", "py37", "py38"]
+skip-string-normalization = true
+
+[build-system]
+
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..ee61e9b
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,15 @@
+[pytest]
+
+addopts =
+ --strict
+
+ -r sxX
+ --show-capture=log
+
+ --cov-report=html
+ --cov-report=term-missing:skip-covered
+ --no-cov-on-fail
+
+cache_dir = .cache
+
+markers =
diff --git a/scent.py b/scent.py
new file mode 100644
index 0000000..9569a6c
--- /dev/null
+++ b/scent.py
@@ -0,0 +1,95 @@
+"""Configuration file for sniffer."""
+
+import time
+import subprocess
+
+from sniffer.api import select_runnable, file_validator, runnable
+try:
+ from pync import Notifier
+except ImportError:
+ notify = None
+else:
+ notify = Notifier.notify
+
+
+watch_paths = ["defipulsedata", "tests"]
+
+
+class Options:
+ group = int(time.time()) # unique per run
+ show_coverage = False
+ rerun_args = None
+
+ targets = [
+ (('make', 'test-all'), "Integration Tests", False),
+ (('make', 'check'), "Static Analysis", True),
+ (('make', 'docs'), None, True),
+ ]
+
+
+@select_runnable('run_targets')
+@file_validator
+def python_files(filename):
+ return filename.endswith('.py') and '.py.' not in filename
+
+
+@select_runnable('run_targets')
+@file_validator
+def html_files(filename):
+ return filename.split('.')[-1] in ['html', 'css', 'js']
+
+
+@runnable
+def run_targets(*args):
+ """Run targets for Python."""
+ Options.show_coverage = 'coverage' in args
+
+ count = 0
+ for count, (command, title, retry) in enumerate(Options.targets, start=1):
+
+ success = call(command, title, retry)
+ if not success:
+ message = "✅ " * (count - 1) + "❌"
+ show_notification(message, title)
+
+ return False
+
+ message = "✅ " * count
+ title = "All Targets"
+ show_notification(message, title)
+ show_coverage()
+
+ return True
+
+
+def call(command, title, retry):
+ """Run a command-line program and display the result."""
+ if Options.rerun_args:
+ command, title, retry = Options.rerun_args
+ Options.rerun_args = None
+ success = call(command, title, retry)
+ if not success:
+ return False
+
+ print("")
+ print("$ %s" % ' '.join(command))
+ failure = subprocess.call(command)
+
+ if failure and retry:
+ Options.rerun_args = command, title, retry
+
+ return not failure
+
+
+def show_notification(message, title):
+ """Show a user notification."""
+ if notify and title:
+ notify(message, title=title, group=Options.group)
+
+
+def show_coverage():
+ """Launch the coverage report."""
+ if Options.show_coverage:
+ subprocess.call(['make', 'read-coverage'])
+
+ Options.show_coverage = False
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..2a67748
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Unit tests for the package."""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..4051715
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,11 @@
+"""Unit tests configuration file."""
+
+import log
+
+
+def pytest_configure(config):
+ """Disable verbose output when running tests."""
+ log.init(debug=True)
+
+ terminal = config.pluginmanager.getplugin('terminal')
+ terminal.TerminalReporter.showfspath = False
diff --git a/tests/test_defi_pulse.py b/tests/test_defi_pulse.py
new file mode 100644
index 0000000..600042b
--- /dev/null
+++ b/tests/test_defi_pulse.py
@@ -0,0 +1,127 @@
+import unittest
+
+import responses
+
+from defipulsedata import DefiPulse
+
+
+EMPTY_BLOB = {}
+
+
+class TestWrapper(unittest.TestCase):
+ @responses.activate
+ def test_simple_endpoints(self):
+ client = DefiPulse(api_key='mock-key')
+
+ simple_endpoint_urls = [
+ (
+ client.get_market_data,
+ 'https://data-api.defipulse.com/api/v1/defipulse/api/MarketData?api-key=mock-key',
+ ),
+ (
+ client.get_projects,
+ 'https://data-api.defipulse.com/api/v1/defipulse/api/GetProjects?api-key=mock-key',
+ ),
+ (
+ client.get_lending_tokens,
+ 'https://data-api.defipulse.com/api/v1/defipulse/api/GetLendingTokens?api-key=mock-key',
+ ),
+ (
+ client.get_lending_market_data,
+ 'https://data-api.defipulse.com/api/v1/defipulse/api/LendingMarketData?api-key=mock-key',
+ ),
+ (
+ client.get_lending_projects,
+ 'https://data-api.defipulse.com/api/v1/defipulse/api/GetLendingProjects?api-key=mock-key',
+ ),
+ ]
+
+ for fn, url in simple_endpoint_urls:
+ responses.reset()
+ responses.add(responses.GET, url, json=EMPTY_BLOB, status=200)
+ fn()
+ self.assertEqual(responses.calls[0].request.url, url)
+
+ @responses.activate
+ def test_get_history(self):
+ client = DefiPulse(api_key='mock-key')
+
+ url = 'https://data-api.defipulse.com/api/v1/defipulse/api/GetHistory?api-key=mock-key'
+ responses.add(responses.GET, url, json=EMPTY_BLOB, status=200)
+ client.get_history()
+ self.assertEqual(responses.calls[0].request.url, url)
+
+ responses.reset()
+ url_with_invalid_param_combination = 'https://data-api.defipulse.com/api/v1/defipulse/api/GetHistory?period=period&length=length&api-key=mock-key'
+ responses.add(
+ responses.GET,
+ url_with_invalid_param_combination,
+ json=EMPTY_BLOB,
+ status=200,
+ )
+
+ client.get_history(params={'period': 'period', 'length': 'length'})
+
+ self.assertEqual(
+ responses.calls[0].request.url,
+ url_with_invalid_param_combination,
+ )
+
+ self.assertWarnsRegex(
+ UserWarning, 'API only supports "period" or "length" params exclusively.'
+ )
+
+ @responses.activate
+ def test_get_lending_history(self):
+ client = DefiPulse(api_key='mock-key')
+
+ url = 'https://data-api.defipulse.com/api/v1/defipulse/api/getLendingHistory?api-key=mock-key'
+ responses.add(responses.GET, url, json=EMPTY_BLOB, status=200)
+ client.get_lending_history()
+ self.assertEqual(
+ responses.calls[0].request.url,
+ url,
+ )
+
+ responses.reset()
+ url_with_invalid_param_combination = 'https://data-api.defipulse.com/api/v1/defipulse/api/getLendingHistory?period=period&length=length&api-key=mock-key'
+ responses.add(
+ responses.GET,
+ url_with_invalid_param_combination,
+ json=EMPTY_BLOB,
+ status=200,
+ )
+
+ client.get_lending_history(params={'period': 'period', 'length': 'length'})
+
+ self.assertEqual(
+ responses.calls[0].request.url,
+ url_with_invalid_param_combination,
+ )
+
+ self.assertWarnsRegex(
+ UserWarning, 'API only supports "period" or "length" params exclusively.'
+ )
+
+ @responses.activate
+ def test_get_rates(self):
+ client = DefiPulse(api_key='mock-key')
+
+ url_without_amount = 'https://data-api.defipulse.com/api/v1/defipulse/api/GetRates?token=DAI&api-key=mock-key'
+ responses.add(responses.GET, url_without_amount, json=EMPTY_BLOB, status=200)
+ client.get_rates(token='DAI')
+ self.assertEqual(
+ responses.calls[0].request.url,
+ url_without_amount,
+ 'it does not include amount as a query param',
+ )
+
+ responses.reset()
+ url_with_amount = 'https://data-api.defipulse.com/api/v1/defipulse/api/GetRates?token=DAI&amount=100&api-key=mock-key'
+ responses.add(responses.GET, url_with_amount, json=EMPTY_BLOB, status=200)
+ client.get_rates(token='DAI', amount=100)
+ self.assertEqual(
+ responses.calls[0].request.url,
+ url_with_amount,
+ 'it includes the amount as a query param',
+ )
diff --git a/tests/test_dex_ag.py b/tests/test_dex_ag.py
new file mode 100644
index 0000000..a3c8f56
--- /dev/null
+++ b/tests/test_dex_ag.py
@@ -0,0 +1,124 @@
+import unittest
+
+import responses
+
+from defipulsedata import DexAg
+
+
+class TestWrapper(unittest.TestCase):
+ @responses.activate
+ def test_get_markets(self):
+ expected_url = (
+ 'https://data-api.defipulse.com/api/v1/dexag/markets?api-key=mock-key'
+ )
+
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ DexAg(api_key='mock-key').get_markets()
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ @responses.activate
+ def test_get_token_list_full(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/dexag/token-list-full?api-key=mock-key'
+
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ DexAg(api_key='mock-key').get_token_list_full()
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+
+class GetPriceTestWrapper(unittest.TestCase):
+ @responses.activate
+ def test_denomination_in_to_token(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/dexag/price?toAmount=1&from=ETH&to=DAI&dex=all&api-key=mock-key'
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ DexAg(api_key='mock-key').get_price(
+ fromToken='ETH', toToken='DAI', params={'toAmount': 1}
+ )
+ self.assertEqual(
+ responses.calls[0].request.url,
+ expected_url,
+ 'it serializes toAmount in the query params',
+ )
+
+ @responses.activate
+ def test_denomination_in_from_token(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/dexag/price?fromAmount=1&from=ETH&to=DAI&dex=all&api-key=mock-key'
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ DexAg(api_key='mock-key').get_price(
+ fromToken='ETH', toToken='DAI', params={'fromAmount': 1}
+ )
+ self.assertEqual(
+ responses.calls[0].request.url,
+ expected_url,
+ 'it serializes fromAmount in the query params.',
+ )
+
+ @responses.activate
+ def test_all_params(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/dexag/price?discluded=uniswap%2Csushiswap&fromAmount=1&from=ETH&to=DAI&dex=all&api-key=mock-key'
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ params = {'discluded': 'uniswap,sushiswap', 'fromAmount': 1}
+ DexAg(api_key='mock-key').get_price(
+ fromToken='ETH', toToken='DAI', params=params
+ )
+ self.assertEqual(
+ responses.calls[0].request.url,
+ expected_url,
+ 'it includes the params keys and values in the URL',
+ )
+
+ @responses.activate
+ def test_param_overrides(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/dexag/price?fromAmount=1&dex=all&api-key=mock-key&discluded=override-discluded&from=ETH&to=DAI'
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+
+ all_query_params = {
+ 'fromAmount': 1,
+ 'dex': 'override-dex',
+ 'api-key': 'override-key',
+ 'discluded': 'override-discluded',
+ }
+ DexAg(api_key='mock-key').get_price(
+ fromToken='ETH',
+ toToken='DAI',
+ params={**all_query_params, 'from': 'from-override', 'to': 'to-override'},
+ )
+ self.assertEqual(
+ responses.calls[0].request.url,
+ expected_url,
+ 'specifying from and to in the params hash does not affect the URL',
+ )
+
+ def test_invalid_param_combinations(self):
+ client = DexAg(api_key='mock_key')
+ args = {
+ 'fromToken': 'ETH',
+ 'toToken': 'DAI',
+ 'params': {
+ 'fromAmount': 100,
+ 'toAmount': 200,
+ },
+ }
+
+ self.assertRaisesRegex(
+ ValueError,
+ "Only one of from_amount or to_amount may be specified.",
+ client.get_price,
+ **args,
+ )
+
+ self.assertRaisesRegex(
+ ValueError,
+ "Either from_amount or to_amount must be specified.",
+ client.get_price,
+ fromToken='ETH',
+ toToken='DAI',
+ )
+
+ self.assertRaisesRegex(
+ ValueError,
+ "Received unexpected param: unknown-key",
+ client.get_price,
+ fromToken='ETH',
+ toToken='DAI',
+ params={'unknown-key': 'val', 'fromAmount': '1'},
+ )
diff --git a/tests/test_eth_gas_station.py b/tests/test_eth_gas_station.py
new file mode 100644
index 0000000..9a5ae01
--- /dev/null
+++ b/tests/test_eth_gas_station.py
@@ -0,0 +1,23 @@
+import unittest
+
+import responses
+
+from defipulsedata import EthGasStation
+
+
+class TestWrapper(unittest.TestCase):
+ @responses.activate
+ def test_get_gas_price(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/egs/api/ethgasAPI.json?api-key=mock-key'
+
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ EthGasStation(api_key='mock-key').get_gas_price()
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ @responses.activate
+ def test_get_prediction_table(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/egs/api/predictTable.json?api-key=mock-key'
+
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ EthGasStation(api_key='mock-key').get_prediction_table()
+ self.assertEqual(responses.calls[0].request.url, expected_url)
diff --git a/tests/test_pools_fyi.py b/tests/test_pools_fyi.py
new file mode 100644
index 0000000..f3c58c3
--- /dev/null
+++ b/tests/test_pools_fyi.py
@@ -0,0 +1,84 @@
+import unittest
+
+import responses
+
+from defipulsedata import PoolsFyi
+
+
+class TestWrapper(unittest.TestCase):
+ @responses.activate
+ def test_get_exchanges(self):
+ url_without_params = 'https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/exchanges?api-key=mock-key'
+ responses.add(responses.GET, url_without_params, json='{}', status=200)
+ PoolsFyi(api_key='mock-key').get_exchanges()
+ self.assertEqual(responses.calls[0].request.url, url_without_params)
+
+ responses.reset()
+ url_with_params = 'https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/exchanges?tags=stable&platform=bancor&direction=asc&orderBy=platform&offset=1&limit=200&api-key=mock-key'
+ all_params = {
+ 'tags': 'stable',
+ 'platform': 'bancor',
+ 'direction': 'asc',
+ 'orderBy': 'platform',
+ 'offset': 1,
+ 'limit': 200,
+ }
+ responses.add(responses.GET, url_with_params, json='{}', status=200)
+ PoolsFyi(api_key='mock-key').get_exchanges(params=all_params)
+ self.assertEqual(
+ responses.calls[0].request.url,
+ url_with_params,
+ 'it correctly serializes the query params',
+ )
+
+ @responses.activate
+ def test_get_returns(self):
+ address = '0x0000000000000000000000000000000000000000'
+ expected_url = 'https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/returns/0x0000000000000000000000000000000000000000?api-key=mock-key'
+
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ PoolsFyi(api_key='mock-key').get_returns(address=address)
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ @responses.activate
+ def test_get_liquidity(self):
+ address = '0x0000000000000000000000000000000000000000'
+ expected_url = 'https://data-api.defipulse.com/api/v1/blocklytics/pools/v0/liquidity/0x0000000000000000000000000000000000000000?api-key=mock-key'
+
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ PoolsFyi(api_key='mock-key').get_liquidity(address=address)
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ @responses.activate
+ def test_get_exchange(self):
+ address = '0x0000000000000000000000000000000000000000'
+ expected_url = 'https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/exchange/0x0000000000000000000000000000000000000000?api-key=mock-key'
+
+ responses.add(responses.GET, expected_url, json='{}', status=200)
+ PoolsFyi(api_key='mock-key').get_exchange(address=address)
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ @responses.activate
+ def test_get_trades(self):
+ address = '0x0000000000000000000000000000000000000000'
+ url_without_params = 'https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/trades/0x0000000000000000000000000000000000000000?api-key=mock-key'
+
+ responses.add(responses.GET, url_without_params, json='{}', status=200)
+ PoolsFyi(api_key='mock-key').get_trades(address=address)
+ self.assertEqual(responses.calls[0].request.url, url_without_params)
+
+ responses.reset()
+ url_with_all_params = 'https://data-api.defipulse.com/api/v1/blocklytics/pools/v1/trades/0x0000000000000000000000000000000000000000?from=2020-10-21&to=2020-10-31&platform=bancor&direction=asc&orderBy=platform&offset=1&limit=200&api-key=mock-key'
+ responses.add(responses.GET, url_with_all_params, json='{}', status=200)
+
+ all_params = {
+ 'from': '2020-10-21',
+ 'to': '2020-10-31',
+ 'platform': 'bancor',
+ 'direction': 'asc',
+ 'orderBy': 'platform',
+ 'offset': 1,
+ 'limit': 200,
+ }
+ PoolsFyi(api_key='mock-key').get_trades(address=address, params=all_params)
+ self.assertEqual(responses.calls[0].request.url, url_with_all_params)
diff --git a/tests/test_rek_to.py b/tests/test_rek_to.py
new file mode 100644
index 0000000..6a75ab1
--- /dev/null
+++ b/tests/test_rek_to.py
@@ -0,0 +1,41 @@
+import unittest
+
+import responses
+
+from defipulsedata import RekTo
+
+
+EMPTY_DICT = {}
+
+
+class TestWrapper(unittest.TestCase):
+ @responses.activate
+ def test_get_events(self):
+ expected_url = (
+ 'https://data-api.defipulse.com/api/v1/rekto/api/events?api-key=mock-key'
+ )
+ client = RekTo(api_key='mock-key')
+ responses.add(responses.GET, expected_url, json=EMPTY_DICT, status=200)
+
+ client.get_events()
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ @responses.activate
+ def test_get_top_10(self):
+ expected_url = (
+ 'https://data-api.defipulse.com/api/v1/rekto/api/top10?api-key=mock-key'
+ )
+ client = RekTo(api_key='mock-key')
+ responses.add(responses.GET, expected_url, json=EMPTY_DICT, status=200)
+
+ client.get_top_10()
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ @responses.activate
+ def test_get_total_damage(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/rekto/api/total-damage?api-key=mock-key'
+ client = RekTo(api_key='mock-key')
+ responses.add(responses.GET, expected_url, json=EMPTY_DICT, status=200)
+
+ client.get_total_damage()
+ self.assertEqual(responses.calls[0].request.url, expected_url)
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..691aac7
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,47 @@
+import unittest
+
+import requests
+import responses
+
+from defipulsedata import utils
+
+
+EMPTY_DICT = {}
+
+
+class TestWrapper(unittest.TestCase):
+ @responses.activate
+ def test_get_request(self):
+ expected_url = 'https://data-api.defipulse.com/api/v1/egs/api/ethgasAPI.json?api-key=mock-key'
+
+ responses.add(responses.GET, expected_url, json=EMPTY_DICT, status=500)
+ self.assertRaises(requests.HTTPError, utils.get_request, expected_url)
+
+ responses.reset()
+ responses.add(responses.GET, expected_url, json=EMPTY_DICT, status=400)
+ self.assertRaises(requests.HTTPError, utils.get_request, expected_url)
+
+ responses.reset()
+ responses.add(responses.GET, expected_url, json=EMPTY_DICT, status=200)
+ utils.get_request(expected_url)
+ self.assertEqual(responses.calls[0].request.url, expected_url)
+
+ def test_validate_allowed_params(self):
+ empty_params = {}
+ params = {'foo': 'bar'}
+
+ self.assertRaises(
+ ValueError, utils.validate_allowed_params, params, empty_params
+ )
+
+ self.assertEqual(
+ utils.validate_allowed_params(empty_params, params),
+ None,
+ 'it handles empty hash input',
+ )
+
+ self.assertEqual(
+ utils.validate_allowed_params(empty_params, None),
+ None,
+ 'it handles None input',
+ )