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). + +[![Unix Build Status](https://img.shields.io/travis/com/jhhb/pydefipulsedata.svg?label=unix)](https://travis-ci.com/jhhb/pydefipulsedata) +[![Windows Build Status](https://img.shields.io/appveyor/ci/jhhb/pydefipulsedata.svg?label=windows)](https://ci.appveyor.com/project/jhhb/pydefipulsedata) +[![Coverage Status](https://img.shields.io/coveralls/jhhb/pydefipulsedata.svg)](https://coveralls.io/r/jhhb/pydefipulsedata) +[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/jhhb/pydefipulsedata.svg)](https://scrutinizer-ci.com/g/jhhb/pydefipulsedata) +[![PyPI Version](https://img.shields.io/pypi/v/defipulsedata.svg)](https://pypi.org/project/defipulsedata) +[![PyPI License](https://img.shields.io/pypi/l/defipulsedata.svg)](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', + )