From b42ceb4cea9c92a3c7864588d8a898c7fb91f430 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 5 Aug 2020 16:30:44 +0200 Subject: [PATCH 01/14] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7981fc3..f4a571b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ target-version = ["py38"] [tool.poetry] name = "urban-meal-delivery" -version = "0.1.0" +version = "0.2.0.dev0" authors = ["Alexander Hess "] description = "Optimizing an urban meal delivery platform" From 9456f86d6575644e1a202161c69e08c2e8a707a4 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Sat, 8 Aug 2020 14:43:02 +0200 Subject: [PATCH 02/14] Add a config object - add the following file: + src/urban_meal_delivery/_config.py - a config module is created holding two sets of configurations: + production => against the real database + testing => against a database with test data - the module is "protected" (i.e., underscore) and imported at the top level via a proxy-like object `config` that detects in which of the two environments the package is being run --- .gitignore | 1 + poetry.lock | 17 +++++- pyproject.toml | 1 + setup.cfg | 15 ++++- src/urban_meal_delivery/__init__.py | 7 +++ src/urban_meal_delivery/_config.py | 87 +++++++++++++++++++++++++++++ tests/test_config.py | 45 +++++++++++++++ 7 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 src/urban_meal_delivery/_config.py create mode 100644 tests/test_config.py diff --git a/.gitignore b/.gitignore index 2e72e0b..84d0bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .cache/ *.egg-info/ +.env .python-version .venv/ diff --git a/poetry.lock b/poetry.lock index 195fcc9..d526e2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -804,6 +804,17 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] +[[package]] +category = "main" +description = "Add .env support to your django/flask apps in development and deployments" +name = "python-dotenv" +optional = false +python-versions = "*" +version = "0.14.0" + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] category = "dev" description = "World timezone definitions, modern and historical" @@ -1140,7 +1151,7 @@ optional = ["pygments", "colorama"] tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11"] [metadata] -content-hash = "beb356287b912e1ce67c5997bd4d683dbc424ee24660a93d8f1fb4e511972762" +content-hash = "862a0bc650dab485af541f185ed3c1e86a8947e7518c8fbcacfd19d5b8055af5" lock-version = "1.0" python-versions = "^3.8" @@ -1526,6 +1537,10 @@ pytest-cov = [ {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, ] +python-dotenv = [ + {file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"}, + {file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"}, +] pytz = [ {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, diff --git a/pyproject.toml b/pyproject.toml index f4a571b..e636874 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ repository = "https://github.com/webartifex/urban-meal-delivery" python = "^3.8" click = "^7.1.2" +python-dotenv = "^0.14.0" [tool.poetry.dev-dependencies] # Task Runners diff --git a/setup.cfg b/setup.cfg index 3e1b9bf..3388dce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -109,6 +109,11 @@ per-file-ignores = WPS213, # No overuse of string constants (e.g., '--version'). WPS226, + src/urban_meal_delivery/_config.py: + # Allow upper case class variables within classes. + WPS115, + # Numbers are normal in config files. + WPS432, tests/*.py: # Type annotations are not strictly enforced. ANN0, ANN2, @@ -135,10 +140,10 @@ show-source = true # wemake-python-styleguide's settings # =================================== allowed-domain-names = + obj, param, result, value, -min-name-length = 3 max-name-length = 40 # darglint strictness = long @@ -186,7 +191,13 @@ single_line_exclusions = typing [mypy] cache_dir = .cache/mypy -[mypy-nox.*,packaging,pytest] +[mypy-dotenv] +ignore_missing_imports = true +[mypy-nox.*] +ignore_missing_imports = true +[mypy-packaging] +ignore_missing_imports = true +[mypy-pytest] ignore_missing_imports = true diff --git a/src/urban_meal_delivery/__init__.py b/src/urban_meal_delivery/__init__.py index d130f04..9fdf819 100644 --- a/src/urban_meal_delivery/__init__.py +++ b/src/urban_meal_delivery/__init__.py @@ -6,8 +6,11 @@ Example: True """ +import os as _os from importlib import metadata as _metadata +from urban_meal_delivery import _config # noqa:WPS450 + try: _pkg_info = _metadata.metadata(__name__) @@ -21,3 +24,7 @@ else: __author__ = _pkg_info['author'] __pkg_name__ = _pkg_info['name'] __version__ = _pkg_info['version'] + + +# Little Hack: "Overwrites" the config module so that the environment is already set. +config = _config.get_config('testing' if _os.getenv('TESTING') else 'production') diff --git a/src/urban_meal_delivery/_config.py b/src/urban_meal_delivery/_config.py new file mode 100644 index 0000000..1be41ea --- /dev/null +++ b/src/urban_meal_delivery/_config.py @@ -0,0 +1,87 @@ +"""Provide package-wide configuration. + +This module is "protected" so that it is only used +via the `config` proxy at the package's top level. + +That already loads the correct configuration +depending on the current environment. +""" + +import datetime +import os +import warnings + +import dotenv + + +dotenv.load_dotenv() + + +class Config: + """Configuration that applies in all situations.""" + + # pylint:disable=too-few-public-methods + + CUTOFF_DAY = datetime.datetime(2017, 2, 1) + + # If a scheduled pre-order is made within this + # time horizon, we treat it as an ad-hoc order. + QUASI_AD_HOC_LIMIT = datetime.timedelta(minutes=45) + + DATABASE_URI = os.getenv('DATABASE_URI') + + # The PostgreSQL schema that holds the tables with the original data. + ORIGINAL_SCHEMA = os.getenv('ORIGINAL_SCHEMA') or 'public' + + # The PostgreSQL schema that holds the tables with the cleaned data. + CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA') or 'clean' + + def __repr__(self) -> str: + """Non-literal text representation.""" + return '' + + +class ProductionConfig(Config): + """Configuration for the real dataset.""" + + # pylint:disable=too-few-public-methods + + TESTING = False + + +class TestingConfig(Config): + """Configuration for the test suite.""" + + # pylint:disable=too-few-public-methods + + TESTING = True + + DATABASE_URI = os.getenv('DATABASE_URI_TESTING') or Config.DATABASE_URI + CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA_TESTING') or Config.CLEAN_SCHEMA + + +def get_config(env: str = 'production') -> Config: + """Get the configuration for the package. + + Args: + env: either 'production' or 'testing'; defaults to the first + + Returns: + config: a namespace with all configurations + + Raises: + ValueError: if `env` is not as specified + """ # noqa:DAR203 + config: Config + if env.strip().lower() == 'production': + config = ProductionConfig() + elif env.strip().lower() == 'testing': + config = TestingConfig() + else: + raise ValueError("Must be either 'production' or 'testing'") + + # Without a PostgreSQL database the package cannot work. + if config.DATABASE_URI is None: + warnings.warn('Bad configurartion: no DATABASE_URI set in the environment') + + return config diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..8983808 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,45 @@ +"""Test the package's configuration module.""" + +import pytest + +from urban_meal_delivery import _config as config_mod # noqa:WPS450 + + +envs = ['production', 'testing'] + + +@pytest.mark.parametrize('env', envs) +def test_config_repr(env): + """Config objects have the text representation ''.""" + config = config_mod.get_config(env) + + assert str(config) == '' + + +def test_invalid_config(): + """There are only 'production' and 'testing' configurations.""" + with pytest.raises(ValueError, match="'production' or 'testing'"): + config_mod.get_config('invalid') + + +@pytest.mark.parametrize('env', envs) +def test_database_uri_set(env, monkeypatch): + """Package does NOT emit warning if DATABASE_URI is set.""" + uri = 'postgresql://user:password@localhost/db' + monkeypatch.setattr(config_mod.ProductionConfig, 'DATABASE_URI', uri) + monkeypatch.setattr(config_mod.TestingConfig, 'DATABASE_URI', uri) + + with pytest.warns(None) as record: + config_mod.get_config(env) + + assert len(record) == 0 # noqa:WPS441,WPS507 + + +@pytest.mark.parametrize('env', envs) +def test_no_database_uri_set(env, monkeypatch): + """Package does not work without DATABASE_URI set in the environment.""" + monkeypatch.setattr(config_mod.ProductionConfig, 'DATABASE_URI', None) + monkeypatch.setattr(config_mod.TestingConfig, 'DATABASE_URI', None) + + with pytest.warns(UserWarning, match='no DATABASE_URI'): + config_mod.get_config(env) From d219fa816d8301e4df3f83f3fa66869eb90c796c Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Sat, 8 Aug 2020 14:50:12 +0200 Subject: [PATCH 03/14] Pin the dependencies ... ... after upgrading: - flake8-plugin-utils - sphinx --- poetry.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index d526e2e..70824d7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -410,7 +410,7 @@ description = "The package provides base classes and utils for flake8 plugin wri name = "flake8-plugin-utils" optional = false python-versions = ">=3.6,<4.0" -version = "1.3.0" +version = "1.3.1" [[package]] category = "dev" @@ -429,10 +429,10 @@ description = "A flake8 plugin checking common style issues or inconsistencies w name = "flake8-pytest-style" optional = false python-versions = ">=3.6,<4.0" -version = "1.2.2" +version = "1.2.3" [package.dependencies] -flake8-plugin-utils = ">=1.3.0,<2.0.0" +flake8-plugin-utils = ">=1.3.1,<2.0.0" [[package]] category = "dev" @@ -898,7 +898,7 @@ description = "Python documentation generator" name = "sphinx" optional = false python-versions = ">=3.5" -version = "3.1.2" +version = "3.2.0" [package.dependencies] Jinja2 = ">=2.3" @@ -1331,16 +1331,16 @@ flake8-isort = [ {file = "flake8_isort-3.0.1-py2.py3-none-any.whl", hash = "sha256:df1dd6dd73f6a8b128c9c783356627231783cccc82c13c6dc343d1a5a491699b"}, ] flake8-plugin-utils = [ - {file = "flake8-plugin-utils-1.3.0.tar.gz", hash = "sha256:965931e7c17a760915e38bb10dc60516b414ef8210e987252a8d73dcb196a5f5"}, - {file = "flake8_plugin_utils-1.3.0-py3-none-any.whl", hash = "sha256:305461c4fbf94877bcc9ccf435771b135d72a40eefd92e70a4b5f761ca43b1c8"}, + {file = "flake8-plugin-utils-1.3.1.tar.gz", hash = "sha256:6e996bc24ebe327558f24efd106f1be5f0c033c8cbb6eed815631f73d487f1c9"}, + {file = "flake8_plugin_utils-1.3.1-py3-none-any.whl", hash = "sha256:efdbf9d15b18f72b7c348dd360f30e7cf3e73aa67ff832d5343eb5aa1115f250"}, ] flake8-polyfill = [ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] flake8-pytest-style = [ - {file = "flake8-pytest-style-1.2.2.tar.gz", hash = "sha256:93830b18d961613a012061867b15b1434e433662197ec3a7594db8389637d5f9"}, - {file = "flake8_pytest_style-1.2.2-py3-none-any.whl", hash = "sha256:63e93d10b9cca6c73309319e89e7b1a8d316c7d1f12407faca975c498192fde3"}, + {file = "flake8-pytest-style-1.2.3.tar.gz", hash = "sha256:48e5ed79ab33846112331bbf51353c568d8a5f515de49c6cd9dfe5fecdcaa6f3"}, + {file = "flake8_pytest_style-1.2.3-py3-none-any.whl", hash = "sha256:fa6e8706b814b1eedf8f7aa62e26bfc03d0e5178a25d828218165be6c8e52570"}, ] flake8-quotes = [ {file = "flake8-quotes-2.1.2.tar.gz", hash = "sha256:c844c9592940c8926c60f00bc620808912ff2acd34923ab5338f3a5ca618a331"}, @@ -1601,8 +1601,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] sphinx = [ - {file = "Sphinx-3.1.2-py3-none-any.whl", hash = "sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00"}, - {file = "Sphinx-3.1.2.tar.gz", hash = "sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"}, + {file = "Sphinx-3.2.0-py3-none-any.whl", hash = "sha256:f7db5b76c42c8b5ef31853c2de7178ef378b985d7793829ec071e120dac1d0ca"}, + {file = "Sphinx-3.2.0.tar.gz", hash = "sha256:cf2d5bc3c6c930ab0a1fbef3ad8a82994b1bf4ae923f8098a05c7e5516f07177"}, ] sphinx-autodoc-typehints = [ {file = "sphinx-autodoc-typehints-1.11.0.tar.gz", hash = "sha256:bbf0b203f1019b0f9843ee8eef0cff856dc04b341f6dbe1113e37f2ebf243e11"}, From fdcc93a1eaf0efb0d194fb38559841b4397cb940 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Sun, 9 Aug 2020 03:45:19 +0200 Subject: [PATCH 04/14] Add an ORM layer - use SQLAlchemy (and PostgreSQL) to model the ORM layer - add the following models: + Address => modelling all kinds of addresses + City => model the three target cities + Courier => model the UDP's couriers + Customer => model the UDP's customers + Order => model the orders received by the UDP + Restaurant => model the restaurants active on the UDP - so far, the emphasis lies on expression the Foreign Key and Check Constraints that are used to validate the assumptions inherent to the cleanded data - provide database-independent unit tests with 100% coverage - provide additional integration tests ("e2e") that commit data to a PostgreSQL instance to validate that the constraints work - adapt linting rules a bit --- noxfile.py | 2 + poetry.lock | 75 ++- pyproject.toml | 2 + setup.cfg | 17 + src/urban_meal_delivery/__init__.py | 9 +- src/urban_meal_delivery/_config.py | 12 +- src/urban_meal_delivery/db/__init__.py | 11 + src/urban_meal_delivery/db/addresses.py | 82 ++++ src/urban_meal_delivery/db/cities.py | 83 ++++ src/urban_meal_delivery/db/connection.py | 17 + src/urban_meal_delivery/db/couriers.py | 51 +++ src/urban_meal_delivery/db/customers.py | 26 ++ src/urban_meal_delivery/db/meta.py | 22 + src/urban_meal_delivery/db/orders.py | 526 ++++++++++++++++++++++ src/urban_meal_delivery/db/restaurants.py | 42 ++ tests/db/__init__.py | 1 + tests/db/conftest.py | 244 ++++++++++ tests/db/test_addresses.py | 141 ++++++ tests/db/test_cities.py | 99 ++++ tests/db/test_couriers.py | 125 +++++ tests/db/test_customer.py | 51 +++ tests/db/test_orders.py | 397 ++++++++++++++++ tests/db/test_restaurants.py | 80 ++++ tests/test_config.py | 8 + 24 files changed, 2119 insertions(+), 4 deletions(-) create mode 100644 src/urban_meal_delivery/db/__init__.py create mode 100644 src/urban_meal_delivery/db/addresses.py create mode 100644 src/urban_meal_delivery/db/cities.py create mode 100644 src/urban_meal_delivery/db/connection.py create mode 100644 src/urban_meal_delivery/db/couriers.py create mode 100644 src/urban_meal_delivery/db/customers.py create mode 100644 src/urban_meal_delivery/db/meta.py create mode 100644 src/urban_meal_delivery/db/orders.py create mode 100644 src/urban_meal_delivery/db/restaurants.py create mode 100644 tests/db/__init__.py create mode 100644 tests/db/conftest.py create mode 100644 tests/db/test_addresses.py create mode 100644 tests/db/test_cities.py create mode 100644 tests/db/test_couriers.py create mode 100644 tests/db/test_customer.py create mode 100644 tests/db/test_orders.py create mode 100644 tests/db/test_restaurants.py diff --git a/noxfile.py b/noxfile.py index 96cb2f8..b8fd263 100644 --- a/noxfile.py +++ b/noxfile.py @@ -249,6 +249,8 @@ def test(session): '--cov-branch', '--cov-fail-under=100', '--cov-report=term-missing:skip-covered', + '-k', + 'not e2e', PYTEST_LOCATION, ) session.run('pytest', '--version') diff --git a/poetry.lock b/poetry.lock index 70824d7..cac0b0a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -700,6 +700,14 @@ pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" +[[package]] +category = "main" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +name = "psycopg2" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "2.8.5" + [[package]] category = "dev" description = "library with cross-python path, ini-parsing, io, code, log facilities" @@ -1010,6 +1018,26 @@ version = "1.1.4" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +category = "main" +description = "Database Abstraction Library" +name = "sqlalchemy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.18" + +[package.extras] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mysql = ["mysqlclient"] +oracle = ["cx-oracle"] +postgresql = ["psycopg2"] +postgresql_pg8000 = ["pg8000"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql"] + [[package]] category = "dev" description = "Manage dynamic plugins for Python applications" @@ -1151,7 +1179,7 @@ optional = ["pygments", "colorama"] tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11"] [metadata] -content-hash = "862a0bc650dab485af541f185ed3c1e86a8947e7518c8fbcacfd19d5b8055af5" +content-hash = "508cbaa3105e47cac64c68663ed8d4178ee752bf267cb24cf68264e73325e10b" lock-version = "1.0" python-versions = "^3.8" @@ -1501,6 +1529,21 @@ pre-commit = [ {file = "pre_commit-2.6.0-py2.py3-none-any.whl", hash = "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626"}, {file = "pre_commit-2.6.0.tar.gz", hash = "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915"}, ] +psycopg2 = [ + {file = "psycopg2-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:a0984ff49e176062fcdc8a5a2a670c9bb1704a2f69548bce8f8a7bad41c661bf"}, + {file = "psycopg2-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:acf56d564e443e3dea152efe972b1434058244298a94348fc518d6dd6a9fb0bb"}, + {file = "psycopg2-2.8.5-cp34-cp34m-win32.whl", hash = "sha256:440a3ea2c955e89321a138eb7582aa1d22fe286c7d65e26a2c5411af0a88ae72"}, + {file = "psycopg2-2.8.5-cp34-cp34m-win_amd64.whl", hash = "sha256:6b306dae53ec7f4f67a10942cf8ac85de930ea90e9903e2df4001f69b7833f7e"}, + {file = "psycopg2-2.8.5-cp35-cp35m-win32.whl", hash = "sha256:d3b29d717d39d3580efd760a9a46a7418408acebbb784717c90d708c9ed5f055"}, + {file = "psycopg2-2.8.5-cp35-cp35m-win_amd64.whl", hash = "sha256:6a471d4d2a6f14c97a882e8d3124869bc623f3df6177eefe02994ea41fd45b52"}, + {file = "psycopg2-2.8.5-cp36-cp36m-win32.whl", hash = "sha256:27c633f2d5db0fc27b51f1b08f410715b59fa3802987aec91aeb8f562724e95c"}, + {file = "psycopg2-2.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2df2bf1b87305bd95eb3ac666ee1f00a9c83d10927b8144e8e39644218f4cf81"}, + {file = "psycopg2-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:ac5b23d0199c012ad91ed1bbb971b7666da651c6371529b1be8cbe2a7bf3c3a9"}, + {file = "psycopg2-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2c0afb40cfb4d53487ee2ebe128649028c9a78d2476d14a67781e45dc287f080"}, + {file = "psycopg2-2.8.5-cp38-cp38-win32.whl", hash = "sha256:2327bf42c1744a434ed8ed0bbaa9168cac7ee5a22a9001f6fc85c33b8a4a14b7"}, + {file = "psycopg2-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:132efc7ee46a763e68a815f4d26223d9c679953cd190f1f218187cb60decf535"}, + {file = "psycopg2-2.8.5.tar.gz", hash = "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818"}, +] py = [ {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, @@ -1632,6 +1675,36 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, ] +sqlalchemy = [ + {file = "SQLAlchemy-1.3.18-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-win32.whl", hash = "sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27m-win_amd64.whl", hash = "sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1"}, + {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-win32.whl", hash = "sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3"}, + {file = "SQLAlchemy-1.3.18-cp35-cp35m-win_amd64.whl", hash = "sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-win32.whl", hash = "sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299"}, + {file = "SQLAlchemy-1.3.18-cp36-cp36m-win_amd64.whl", hash = "sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-win32.whl", hash = "sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e"}, + {file = "SQLAlchemy-1.3.18-cp37-cp37m-win_amd64.whl", hash = "sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-win32.whl", hash = "sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7"}, + {file = "SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl", hash = "sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8"}, + {file = "SQLAlchemy-1.3.18.tar.gz", hash = "sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7"}, +] stevedore = [ {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, diff --git a/pyproject.toml b/pyproject.toml index e636874..4948d98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,9 @@ repository = "https://github.com/webartifex/urban-meal-delivery" python = "^3.8" click = "^7.1.2" +psycopg2 = "^2.8.5" # adapter for PostgreSQL python-dotenv = "^0.14.0" +sqlalchemy = "^1.3.18" [tool.poetry.dev-dependencies] # Task Runners diff --git a/setup.cfg b/setup.cfg index 3388dce..612d12e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,8 @@ ignore = # If --ignore is passed on the command # line, still ignore the following: extend-ignore = + # Too long line => duplicate with E501. + B950, # Comply with black's style. # Source: https://github.com/psf/black/blob/master/docs/compatible_configs.md#flake8 E203, W503, @@ -114,6 +116,10 @@ per-file-ignores = WPS115, # Numbers are normal in config files. WPS432, + src/urban_meal_delivery/db/addresses.py: + WPS226, + src/urban_meal_delivery/db/orders.py: + WPS226, tests/*.py: # Type annotations are not strictly enforced. ANN0, ANN2, @@ -121,8 +127,12 @@ per-file-ignores = S101, # Shadowing outer scopes occurs naturally with mocks. WPS442, + # Modules may have many test cases. + WPS202,WPS204,WPS214, # No overuse of string constants (e.g., '__version__'). WPS226, + # Numbers are normal in test cases as expected results. + WPS432, # Explicitly set mccabe's maximum complexity to 10 as recommended by # Thomas McCabe, the inventor of the McCabe complexity, and the NIST. @@ -199,6 +209,8 @@ ignore_missing_imports = true ignore_missing_imports = true [mypy-pytest] ignore_missing_imports = true +[mypy-sqlalchemy.*] +ignore_missing_imports = true [pylint.FORMAT] @@ -210,6 +222,9 @@ disable = # We use TODO's to indicate locations in the source base # that must be worked on in the near future. fixme, + # Too many false positives and cannot be disabled within a file. + # Source: https://github.com/PyCQA/pylint/issues/214 + duplicate-code, # Comply with black's style. bad-continuation, bad-whitespace, # ===================== @@ -240,3 +255,5 @@ addopts = --strict-markers cache_dir = .cache/pytest console_output_style = count +markers = + e2e: integration tests, inlc., for example, tests touching a database diff --git a/src/urban_meal_delivery/__init__.py b/src/urban_meal_delivery/__init__.py index 9fdf819..676a458 100644 --- a/src/urban_meal_delivery/__init__.py +++ b/src/urban_meal_delivery/__init__.py @@ -27,4 +27,11 @@ else: # Little Hack: "Overwrites" the config module so that the environment is already set. -config = _config.get_config('testing' if _os.getenv('TESTING') else 'production') +config: _config.Config = _config.get_config( + 'testing' if _os.getenv('TESTING') else 'production', +) + + +# Import `db` down here as it depends on `config`. +# pylint:disable=wrong-import-position +from urban_meal_delivery import db # noqa:E402,F401 isort:skip diff --git a/src/urban_meal_delivery/_config.py b/src/urban_meal_delivery/_config.py index 1be41ea..482b95d 100644 --- a/src/urban_meal_delivery/_config.py +++ b/src/urban_meal_delivery/_config.py @@ -6,9 +6,10 @@ via the `config` proxy at the package's top level. That already loads the correct configuration depending on the current environment. """ - import datetime import os +import random +import string import warnings import dotenv @@ -17,6 +18,13 @@ import dotenv dotenv.load_dotenv() +def random_schema_name() -> str: + """Generate a random PostgreSQL schema name for testing.""" + return ''.join( + random.choice(string.ascii_lowercase) for _ in range(10) # noqa:S311 + ) + + class Config: """Configuration that applies in all situations.""" @@ -57,7 +65,7 @@ class TestingConfig(Config): TESTING = True DATABASE_URI = os.getenv('DATABASE_URI_TESTING') or Config.DATABASE_URI - CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA_TESTING') or Config.CLEAN_SCHEMA + CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA_TESTING') or random_schema_name() def get_config(env: str = 'production') -> Config: diff --git a/src/urban_meal_delivery/db/__init__.py b/src/urban_meal_delivery/db/__init__.py new file mode 100644 index 0000000..8b9f0b4 --- /dev/null +++ b/src/urban_meal_delivery/db/__init__.py @@ -0,0 +1,11 @@ +"""Provide the ORM models and a connection to the database.""" + +from urban_meal_delivery.db.addresses import Address # noqa:F401 +from urban_meal_delivery.db.cities import City # noqa:F401 +from urban_meal_delivery.db.connection import make_engine # noqa:F401 +from urban_meal_delivery.db.connection import make_session_factory # noqa:F401 +from urban_meal_delivery.db.couriers import Courier # noqa:F401 +from urban_meal_delivery.db.customers import Customer # noqa:F401 +from urban_meal_delivery.db.meta import Base # noqa:F401 +from urban_meal_delivery.db.orders import Order # noqa:F401 +from urban_meal_delivery.db.restaurants import Restaurant # noqa:F401 diff --git a/src/urban_meal_delivery/db/addresses.py b/src/urban_meal_delivery/db/addresses.py new file mode 100644 index 0000000..d9bfa48 --- /dev/null +++ b/src/urban_meal_delivery/db/addresses.py @@ -0,0 +1,82 @@ +"""Provide the ORM's Address model.""" + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.dialects import postgresql +from sqlalchemy.ext import hybrid + +from urban_meal_delivery.db import meta + + +class Address(meta.Base): + """An Address of a Customer or a Restaurant on the UDP.""" + + __tablename__ = 'addresses' + + # Columns + id = sa.Column(sa.Integer, primary_key=True, autoincrement=False) # noqa:WPS125 + _primary_id = sa.Column('primary_id', sa.Integer, nullable=False, index=True) + created_at = sa.Column(sa.DateTime, nullable=False) + place_id = sa.Column( + sa.Unicode(length=120), nullable=False, index=True, # noqa:WPS432 + ) + latitude = sa.Column(postgresql.DOUBLE_PRECISION, nullable=False) + longitude = sa.Column(postgresql.DOUBLE_PRECISION, nullable=False) + _city_id = sa.Column('city_id', sa.SmallInteger, nullable=False, index=True) + city_name = sa.Column('city', sa.Unicode(length=25), nullable=False) # noqa:WPS432 + zip_code = sa.Column(sa.Integer, nullable=False, index=True) + street = sa.Column(sa.Unicode(length=80), nullable=False) # noqa:WPS432 + floor = sa.Column(sa.SmallInteger) + + # Constraints + __table_args__ = ( + sa.ForeignKeyConstraint( + ['primary_id'], ['addresses.id'], onupdate='RESTRICT', ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['city_id'], ['cities.id'], onupdate='RESTRICT', ondelete='RESTRICT', + ), + sa.CheckConstraint( + '-90 <= latitude AND latitude <= 90', name='latitude_between_90_degrees', + ), + sa.CheckConstraint( + '-180 <= longitude AND longitude <= 180', + name='longitude_between_180_degrees', + ), + sa.CheckConstraint( + '30000 <= zip_code AND zip_code <= 99999', name='valid_zip_code', + ), + sa.CheckConstraint('0 <= floor AND floor <= 40', name='realistic_floor'), + ) + + # Relationships + city = orm.relationship('City', back_populates='addresses') + restaurant = orm.relationship('Restaurant', back_populates='address', uselist=False) + orders_picked_up = orm.relationship( + 'Order', + back_populates='pickup_address', + foreign_keys='[Order._pickup_address_id]', + ) + + orders_delivered = orm.relationship( + 'Order', + back_populates='delivery_address', + foreign_keys='[Order._delivery_address_id]', + ) + + def __repr__(self) -> str: + """Non-literal text representation.""" + return '<{cls}({street} in {city})>'.format( + cls=self.__class__.__name__, street=self.street, city=self.city_name, + ) + + @hybrid.hybrid_property + def is_primary(self) -> bool: + """If an Address object is the earliest one entered at its location. + + Street addresses may have been entered several times with different + versions/spellings of the street name and/or different floors. + + `is_primary` indicates the first in a group of addresses. + """ + return self.id == self._primary_id diff --git a/src/urban_meal_delivery/db/cities.py b/src/urban_meal_delivery/db/cities.py new file mode 100644 index 0000000..00305b2 --- /dev/null +++ b/src/urban_meal_delivery/db/cities.py @@ -0,0 +1,83 @@ +"""Provide the ORM's City model.""" + +from typing import Dict + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.dialects import postgresql + +from urban_meal_delivery.db import meta + + +class City(meta.Base): + """A City where the UDP operates in.""" + + __tablename__ = 'cities' + + # Generic columns + id = sa.Column( # noqa:WPS125 + sa.SmallInteger, primary_key=True, autoincrement=False, + ) + name = sa.Column(sa.Unicode(length=10), nullable=False) + kml = sa.Column(sa.UnicodeText, nullable=False) + + # Google Maps related columns + _center_latitude = sa.Column( + 'center_latitude', postgresql.DOUBLE_PRECISION, nullable=False, + ) + _center_longitude = sa.Column( + 'center_longitude', postgresql.DOUBLE_PRECISION, nullable=False, + ) + _northeast_latitude = sa.Column( + 'northeast_latitude', postgresql.DOUBLE_PRECISION, nullable=False, + ) + _northeast_longitude = sa.Column( + 'northeast_longitude', postgresql.DOUBLE_PRECISION, nullable=False, + ) + _southwest_latitude = sa.Column( + 'southwest_latitude', postgresql.DOUBLE_PRECISION, nullable=False, + ) + _southwest_longitude = sa.Column( + 'southwest_longitude', postgresql.DOUBLE_PRECISION, nullable=False, + ) + initial_zoom = sa.Column(sa.SmallInteger, nullable=False) + + # Relationships + addresses = orm.relationship('Address', back_populates='city') + + def __repr__(self) -> str: + """Non-literal text representation.""" + return '<{cls}({name})>'.format(cls=self.__class__.__name__, name=self.name) + + @property + def location(self) -> Dict[str, float]: + """GPS location of the city's center. + + Example: + {"latitude": 48.856614, "longitude": 2.3522219} + """ + return { + 'latitude': self._center_latitude, + 'longitude': self._center_longitude, + } + + @property + def viewport(self) -> Dict[str, Dict[str, float]]: + """Google Maps viewport of the city. + + Example: + { + 'northeast': {'latitude': 48.9021449, 'longitude': 2.4699208}, + 'southwest': {'latitude': 48.815573, 'longitude': 2.225193}, + } + """ # noqa:RST203 + return { + 'northeast': { + 'latitude': self._northeast_latitude, + 'longitude': self._northeast_longitude, + }, + 'southwest': { + 'latitude': self._southwest_latitude, + 'longitude': self._southwest_longitude, + }, + } diff --git a/src/urban_meal_delivery/db/connection.py b/src/urban_meal_delivery/db/connection.py new file mode 100644 index 0000000..460ef9d --- /dev/null +++ b/src/urban_meal_delivery/db/connection.py @@ -0,0 +1,17 @@ +"""Provide connection utils for the ORM layer.""" + +import sqlalchemy as sa +from sqlalchemy import engine +from sqlalchemy import orm + +import urban_meal_delivery + + +def make_engine() -> engine.Engine: # pragma: no cover + """Provide a configured Engine object.""" + return sa.create_engine(urban_meal_delivery.config.DATABASE_URI) + + +def make_session_factory() -> orm.Session: # pragma: no cover + """Provide a configured Session factory.""" + return orm.sessionmaker(bind=make_engine()) diff --git a/src/urban_meal_delivery/db/couriers.py b/src/urban_meal_delivery/db/couriers.py new file mode 100644 index 0000000..be065a5 --- /dev/null +++ b/src/urban_meal_delivery/db/couriers.py @@ -0,0 +1,51 @@ +"""Provide the ORM's Courier model.""" + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.dialects import postgresql + +from urban_meal_delivery.db import meta + + +class Courier(meta.Base): + """A Courier working for the UDP.""" + + # pylint:disable=too-few-public-methods + + __tablename__ = 'couriers' + + # Columns + id = sa.Column(sa.Integer, primary_key=True, autoincrement=False) # noqa:WPS125 + created_at = sa.Column(sa.DateTime, nullable=False) + vehicle = sa.Column(sa.Unicode(length=10), nullable=False) + historic_speed = sa.Column('speed', postgresql.DOUBLE_PRECISION, nullable=False) + capacity = sa.Column(sa.SmallInteger, nullable=False) + pay_per_hour = sa.Column(sa.SmallInteger, nullable=False) + pay_per_order = sa.Column(sa.SmallInteger, nullable=False) + + # Constraints + __table_args__ = ( + sa.CheckConstraint( + "vehicle IN ('bicycle', 'motorcycle')", name='available_vehicle_types', + ), + sa.CheckConstraint('0 <= speed AND speed <= 30', name='realistic_speed'), + sa.CheckConstraint( + '0 <= capacity AND capacity <= 200', name='capacity_under_200_liters', + ), + sa.CheckConstraint( + '0 <= pay_per_hour AND pay_per_hour <= 1500', name='realistic_pay_per_hour', + ), + sa.CheckConstraint( + '0 <= pay_per_order AND pay_per_order <= 650', + name='realistic_pay_per_order', + ), + ) + + # Relationships + orders = orm.relationship('Order', back_populates='courier') + + def __repr__(self) -> str: + """Non-literal text representation.""" + return '<{cls}(#{courier_id})>'.format( + cls=self.__class__.__name__, courier_id=self.id, + ) diff --git a/src/urban_meal_delivery/db/customers.py b/src/urban_meal_delivery/db/customers.py new file mode 100644 index 0000000..e96361a --- /dev/null +++ b/src/urban_meal_delivery/db/customers.py @@ -0,0 +1,26 @@ +"""Provide the ORM's Customer model.""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from urban_meal_delivery.db import meta + + +class Customer(meta.Base): + """A Customer of the UDP.""" + + # pylint:disable=too-few-public-methods + + __tablename__ = 'customers' + + # Columns + id = sa.Column(sa.Integer, primary_key=True, autoincrement=False) # noqa:WPS125 + + def __repr__(self) -> str: + """Non-literal text representation.""" + return '<{cls}(#{customer_id})>'.format( + cls=self.__class__.__name__, customer_id=self.id, + ) + + # Relationships + orders = orm.relationship('Order', back_populates='customer') diff --git a/src/urban_meal_delivery/db/meta.py b/src/urban_meal_delivery/db/meta.py new file mode 100644 index 0000000..94dc143 --- /dev/null +++ b/src/urban_meal_delivery/db/meta.py @@ -0,0 +1,22 @@ +"""Provide the ORM's declarative base.""" + +from typing import Any + +import sqlalchemy as sa +from sqlalchemy.ext import declarative + +import urban_meal_delivery + + +Base: Any = declarative.declarative_base( + metadata=sa.MetaData( + schema=urban_meal_delivery.config.CLEAN_SCHEMA, + naming_convention={ + 'pk': 'pk_%(table_name)s', # noqa:WPS323 + 'fk': 'fk_%(table_name)s_to_%(referred_table_name)s_via_%(column_0_N_name)s', # noqa:E501,WPS323 + 'uq': 'uq_%(table_name)s_on_%(column_0_N_name)s', # noqa:WPS323 + 'ix': 'ix_%(table_name)s_on_%(column_0_N_name)s', # noqa:WPS323 + 'ck': 'ck_%(table_name)s_on_%(constraint_name)s', # noqa:WPS323 + }, + ), +) diff --git a/src/urban_meal_delivery/db/orders.py b/src/urban_meal_delivery/db/orders.py new file mode 100644 index 0000000..5bb617c --- /dev/null +++ b/src/urban_meal_delivery/db/orders.py @@ -0,0 +1,526 @@ +"""Provide the ORM's Order model.""" + +import datetime + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.dialects import postgresql + +from urban_meal_delivery.db import meta + + +class Order(meta.Base): # noqa:WPS214 + """An Order by a Customer of the UDP.""" + + __tablename__ = 'orders' + + # Generic columns + id = sa.Column(sa.Integer, primary_key=True, autoincrement=False) # noqa:WPS125 + _delivery_id = sa.Column('delivery_id', sa.Integer, index=True, unique=True) + _customer_id = sa.Column('customer_id', sa.Integer, nullable=False, index=True) + placed_at = sa.Column(sa.DateTime, nullable=False, index=True) + ad_hoc = sa.Column(sa.Boolean, nullable=False) + scheduled_delivery_at = sa.Column(sa.DateTime, index=True) + scheduled_delivery_at_corrected = sa.Column(sa.Boolean, index=True) + first_estimated_delivery_at = sa.Column(sa.DateTime) + cancelled = sa.Column(sa.Boolean, nullable=False, index=True) + cancelled_at = sa.Column(sa.DateTime) + cancelled_at_corrected = sa.Column(sa.Boolean, index=True) + + # Price-related columns + sub_total = sa.Column(sa.Integer, nullable=False) + delivery_fee = sa.Column(sa.SmallInteger, nullable=False) + total = sa.Column(sa.Integer, nullable=False) + + # Restaurant-related columns + _restaurant_id = sa.Column( + 'restaurant_id', sa.SmallInteger, nullable=False, index=True, + ) + restaurant_notified_at = sa.Column(sa.DateTime) + restaurant_notified_at_corrected = sa.Column(sa.Boolean, index=True) + restaurant_confirmed_at = sa.Column(sa.DateTime) + restaurant_confirmed_at_corrected = sa.Column(sa.Boolean, index=True) + estimated_prep_duration = sa.Column(sa.Integer, index=True) + estimated_prep_duration_corrected = sa.Column(sa.Boolean, index=True) + estimated_prep_buffer = sa.Column(sa.Integer, nullable=False, index=True) + + # Dispatch-related columns + _courier_id = sa.Column('courier_id', sa.Integer, index=True) + dispatch_at = sa.Column(sa.DateTime) + dispatch_at_corrected = sa.Column(sa.Boolean, index=True) + courier_notified_at = sa.Column(sa.DateTime) + courier_notified_at_corrected = sa.Column(sa.Boolean, index=True) + courier_accepted_at = sa.Column(sa.DateTime) + courier_accepted_at_corrected = sa.Column(sa.Boolean, index=True) + utilization = sa.Column(sa.SmallInteger, nullable=False) + + # Pickup-related columns + _pickup_address_id = sa.Column( + 'pickup_address_id', sa.Integer, nullable=False, index=True, + ) + reached_pickup_at = sa.Column(sa.DateTime) + pickup_at = sa.Column(sa.DateTime) + pickup_at_corrected = sa.Column(sa.Boolean, index=True) + pickup_not_confirmed = sa.Column(sa.Boolean) + left_pickup_at = sa.Column(sa.DateTime) + left_pickup_at_corrected = sa.Column(sa.Boolean, index=True) + + # Delivery-related columns + _delivery_address_id = sa.Column( + 'delivery_address_id', sa.Integer, nullable=False, index=True, + ) + reached_delivery_at = sa.Column(sa.DateTime) + delivery_at = sa.Column(sa.DateTime) + delivery_at_corrected = sa.Column(sa.Boolean, index=True) + delivery_not_confirmed = sa.Column(sa.Boolean) + _courier_waited_at_delivery = sa.Column('courier_waited_at_delivery', sa.Boolean) + + # Statistical columns + logged_delivery_distance = sa.Column(sa.SmallInteger, nullable=True) + logged_avg_speed = sa.Column(postgresql.DOUBLE_PRECISION, nullable=True) + logged_avg_speed_distance = sa.Column(sa.SmallInteger, nullable=True) + + # Constraints + __table_args__ = ( + sa.ForeignKeyConstraint( + ['customer_id'], ['customers.id'], onupdate='RESTRICT', ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['restaurant_id'], + ['restaurants.id'], + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['courier_id'], ['couriers.id'], onupdate='RESTRICT', ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['pickup_address_id'], + ['addresses.id'], + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['delivery_address_id'], + ['addresses.id'], + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.CheckConstraint( + """ + (ad_hoc IS TRUE AND scheduled_delivery_at IS NULL) + OR + (ad_hoc IS FALSE AND scheduled_delivery_at IS NOT NULL) + """, + name='either_ad_hoc_or_scheduled_order', + ), + sa.CheckConstraint( + """ + NOT ( + ad_hoc IS TRUE + AND ( + EXTRACT(HOUR FROM placed_at) < 11 + OR + EXTRACT(HOUR FROM placed_at) > 22 + ) + ) + """, + name='ad_hoc_orders_within_business_hours', + ), + sa.CheckConstraint( + """ + NOT ( + ad_hoc IS FALSE + AND ( + ( + EXTRACT(HOUR FROM scheduled_delivery_at) <= 11 + AND + NOT ( + EXTRACT(HOUR FROM scheduled_delivery_at) = 11 + AND + EXTRACT(MINUTE FROM scheduled_delivery_at) = 45 + ) + ) + OR + EXTRACT(HOUR FROM scheduled_delivery_at) > 22 + ) + ) + """, + name='scheduled_orders_within_business_hours', + ), + sa.CheckConstraint( + """ + NOT ( + EXTRACT(EPOCH FROM scheduled_delivery_at - placed_at) < 1800 + ) + """, + name='scheduled_orders_not_within_30_minutes', + ), + sa.CheckConstraint( + """ + NOT ( + cancelled IS FALSE + AND + cancelled_at IS NOT NULL + ) + """, + name='only_cancelled_orders_may_have_cancelled_at', + ), + sa.CheckConstraint( + """ + NOT ( + cancelled IS TRUE + AND + delivery_at IS NOT NULL + ) + """, + name='cancelled_orders_must_not_be_delivered', + ), + sa.CheckConstraint( + '0 <= estimated_prep_duration AND estimated_prep_duration <= 2700', + name='estimated_prep_duration_between_0_and_2700', + ), + sa.CheckConstraint( + 'estimated_prep_duration % 60 = 0', + name='estimated_prep_duration_must_be_whole_minutes', + ), + sa.CheckConstraint( + '0 <= estimated_prep_buffer AND estimated_prep_buffer <= 900', + name='estimated_prep_buffer_between_0_and_900', + ), + sa.CheckConstraint( + 'estimated_prep_buffer % 60 = 0', + name='estimated_prep_buffer_must_be_whole_minutes', + ), + sa.CheckConstraint( + '0 <= utilization AND utilization <= 100', + name='utilization_between_0_and_100', + ), + *( + sa.CheckConstraint( + f""" + ({column} IS NULL AND {column}_corrected IS NULL) + OR + ({column} IS NULL AND {column}_corrected IS TRUE) + OR + ({column} IS NOT NULL AND {column}_corrected IS NOT NULL) + """, + name=f'corrections_only_for_set_value_{index}', + ) + for index, column in enumerate( + ( + 'scheduled_delivery_at', + 'cancelled_at', + 'restaurant_notified_at', + 'restaurant_confirmed_at', + 'estimated_prep_duration', + 'dispatch_at', + 'courier_notified_at', + 'courier_accepted_at', + 'pickup_at', + 'left_pickup_at', + 'delivery_at', + ), + ) + ), + *( + sa.CheckConstraint( + f""" + ({event}_at IS NULL AND {event}_not_confirmed IS NULL) + OR + ({event}_at IS NOT NULL AND {event}_not_confirmed IS NOT NULL) + """, + name=f'{event}_not_confirmed_only_if_{event}', + ) + for event in ('pickup', 'delivery') + ), + sa.CheckConstraint( + """ + (delivery_at IS NULL AND courier_waited_at_delivery IS NULL) + OR + (delivery_at IS NOT NULL AND courier_waited_at_delivery IS NOT NULL) + """, + name='courier_waited_at_delivery_only_if_delivery', + ), + *( + sa.CheckConstraint( + constraint, name='ordered_timestamps_{index}'.format(index=index), + ) + for index, constraint in enumerate( + ( + 'placed_at < scheduled_delivery_at', + 'placed_at < first_estimated_delivery_at', + 'placed_at < cancelled_at', + 'placed_at < restaurant_notified_at', + 'placed_at < restaurant_confirmed_at', + 'placed_at < dispatch_at', + 'placed_at < courier_notified_at', + 'placed_at < courier_accepted_at', + 'placed_at < reached_pickup_at', + 'placed_at < left_pickup_at', + 'placed_at < reached_delivery_at', + 'placed_at < delivery_at', + 'cancelled_at > restaurant_notified_at', + 'cancelled_at > restaurant_confirmed_at', + 'cancelled_at > dispatch_at', + 'cancelled_at > courier_notified_at', + 'cancelled_at > courier_accepted_at', + 'cancelled_at > reached_pickup_at', + 'cancelled_at > pickup_at', + 'cancelled_at > left_pickup_at', + 'cancelled_at > reached_delivery_at', + 'cancelled_at > delivery_at', + 'restaurant_notified_at < restaurant_confirmed_at', + 'restaurant_notified_at < pickup_at', + 'restaurant_confirmed_at < pickup_at', + 'dispatch_at < courier_notified_at', + 'dispatch_at < courier_accepted_at', + 'dispatch_at < reached_pickup_at', + 'dispatch_at < pickup_at', + 'dispatch_at < left_pickup_at', + 'dispatch_at < reached_delivery_at', + 'dispatch_at < delivery_at', + 'courier_notified_at < courier_accepted_at', + 'courier_notified_at < reached_pickup_at', + 'courier_notified_at < pickup_at', + 'courier_notified_at < left_pickup_at', + 'courier_notified_at < reached_delivery_at', + 'courier_notified_at < delivery_at', + 'courier_accepted_at < reached_pickup_at', + 'courier_accepted_at < pickup_at', + 'courier_accepted_at < left_pickup_at', + 'courier_accepted_at < reached_delivery_at', + 'courier_accepted_at < delivery_at', + 'reached_pickup_at < pickup_at', + 'reached_pickup_at < left_pickup_at', + 'reached_pickup_at < reached_delivery_at', + 'reached_pickup_at < delivery_at', + 'pickup_at < left_pickup_at', + 'pickup_at < reached_delivery_at', + 'pickup_at < delivery_at', + 'left_pickup_at < reached_delivery_at', + 'left_pickup_at < delivery_at', + 'reached_delivery_at < delivery_at', + ), + ) + ), + ) + + # Relationships + customer = orm.relationship('Customer', back_populates='orders') + restaurant = orm.relationship('Restaurant', back_populates='orders') + courier = orm.relationship('Courier', back_populates='orders') + pickup_address = orm.relationship( + 'Address', + back_populates='orders_picked_up', + foreign_keys='[Order._pickup_address_id]', + ) + delivery_address = orm.relationship( + 'Address', + back_populates='orders_delivered', + foreign_keys='[Order._delivery_address_id]', + ) + + # Convenience properties + + @property + def scheduled(self) -> bool: + """Inverse of Order.ad_hoc.""" + return not self.ad_hoc + + @property + def completed(self) -> bool: + """Inverse of Order.cancelled.""" + return not self.cancelled + + @property + def corrected(self) -> bool: + """If any timestamp was corrected as compared to the original data.""" + return ( + self.scheduled_delivery_at_corrected # noqa:WPS222 => too much logic + or self.cancelled_at_corrected + or self.restaurant_notified_at_corrected + or self.restaurant_confirmed_at_corrected + or self.dispatch_at_corrected + or self.courier_notified_at_corrected + or self.courier_accepted_at_corrected + or self.pickup_at_corrected + or self.left_pickup_at_corrected + or self.delivery_at_corrected + ) + + # Timing-related properties + + @property + def time_to_accept(self) -> datetime.timedelta: + """Time until a courier accepted an order. + + This adds the time it took the UDP to notify a courier. + """ + if not self.dispatch_at: + raise RuntimeError('dispatch_at is not set') + if not self.courier_accepted_at: + raise RuntimeError('courier_accepted_at is not set') + return self.courier_accepted_at - self.dispatch_at + + @property + def time_to_react(self) -> datetime.timedelta: + """Time a courier took to accept an order. + + This time is a subset of Order.time_to_accept. + """ + if not self.courier_notified_at: + raise RuntimeError('courier_notified_at is not set') + if not self.courier_accepted_at: + raise RuntimeError('courier_accepted_at is not set') + return self.courier_accepted_at - self.courier_notified_at + + @property + def time_to_pickup(self) -> datetime.timedelta: + """Time from a courier's acceptance to arrival at the pickup location.""" + if not self.courier_accepted_at: + raise RuntimeError('courier_accepted_at is not set') + if not self.reached_pickup_at: + raise RuntimeError('reached_pickup_at is not set') + return self.reached_pickup_at - self.courier_accepted_at + + @property + def time_at_pickup(self) -> datetime.timedelta: + """Time a courier stayed at the pickup location.""" + if not self.reached_pickup_at: + raise RuntimeError('reached_pickup_at is not set') + if not self.pickup_at: + raise RuntimeError('pickup_at is not set') + return self.pickup_at - self.reached_pickup_at + + @property + def scheduled_pickup_at(self) -> datetime.datetime: + """Point in time at which the pickup was scheduled.""" + if not self.restaurant_notified_at: + raise RuntimeError('restaurant_notified_at is not set') + if not self.estimated_prep_duration: + raise RuntimeError('estimated_prep_duration is not set') + delta = datetime.timedelta(seconds=self.estimated_prep_duration) + return self.restaurant_notified_at + delta + + @property + def courier_early(self) -> datetime.timedelta: + """Time by which a courier is early for pickup. + + Measured relative to Order.scheduled_pickup_at. + + 0 if the courier is on time or late. + + Goes together with Order.courier_late. + """ + return max( + datetime.timedelta(), self.scheduled_pickup_at - self.reached_pickup_at, + ) + + @property + def courier_late(self) -> datetime.timedelta: + """Time by which a courier is late for pickup. + + Measured relative to Order.scheduled_pickup_at. + + 0 if the courier is on time or early. + + Goes together with Order.courier_early. + """ + return max( + datetime.timedelta(), self.reached_pickup_at - self.scheduled_pickup_at, + ) + + @property + def restaurant_early(self) -> datetime.timedelta: + """Time by which a restaurant is early for pickup. + + Measured relative to Order.scheduled_pickup_at. + + 0 if the restaurant is on time or late. + + Goes together with Order.restaurant_late. + """ + return max(datetime.timedelta(), self.scheduled_pickup_at - self.pickup_at) + + @property + def restaurant_late(self) -> datetime.timedelta: + """Time by which a restaurant is late for pickup. + + Measured relative to Order.scheduled_pickup_at. + + 0 if the restaurant is on time or early. + + Goes together with Order.restaurant_early. + """ + return max(datetime.timedelta(), self.pickup_at - self.scheduled_pickup_at) + + @property + def time_to_delivery(self) -> datetime.timedelta: + """Time a courier took from pickup to delivery location.""" + if not self.pickup_at: + raise RuntimeError('pickup_at is not set') + if not self.reached_delivery_at: + raise RuntimeError('reached_delivery_at is not set') + return self.reached_delivery_at - self.pickup_at + + @property + def time_at_delivery(self) -> datetime.timedelta: + """Time a courier stayed at the delivery location.""" + if not self.reached_delivery_at: + raise RuntimeError('reached_delivery_at is not set') + if not self.delivery_at: + raise RuntimeError('delivery_at is not set') + return self.delivery_at - self.reached_delivery_at + + @property + def courier_waited_at_delivery(self) -> datetime.timedelta: + """Time a courier waited at the delivery location.""" + if self._courier_waited_at_delivery: + return self.time_at_delivery + return datetime.timedelta() + + @property + def delivery_early(self) -> datetime.timedelta: + """Time by which a scheduled order was early. + + Measured relative to Order.scheduled_delivery_at. + + 0 if the delivery is on time or late. + + Goes together with Order.delivery_late. + """ + if not self.scheduled: + raise AttributeError('Makes sense only for scheduled orders') + return max(datetime.timedelta(), self.scheduled_delivery_at - self.delivery_at) + + @property + def delivery_late(self) -> datetime.timedelta: + """Time by which a scheduled order was late. + + Measured relative to Order.scheduled_delivery_at. + + 0 if the delivery is on time or early. + + Goes together with Order.delivery_early. + """ + if not self.scheduled: + raise AttributeError('Makes sense only for scheduled orders') + return max(datetime.timedelta(), self.delivery_at - self.scheduled_delivery_at) + + @property + def total_time(self) -> datetime.timedelta: + """Time from order placement to delivery for an ad-hoc order.""" + if self.scheduled: + raise AttributeError('Scheduled orders have no total_time') + if self.cancelled: + raise RuntimeError('Cancelled orders have no total_time') + return self.delivery_at - self.placed_at + + # Other Methods + + def __repr__(self) -> str: + """Non-literal text representation.""" + return '<{cls}(#{order_id})>'.format( + cls=self.__class__.__name__, order_id=self.id, + ) diff --git a/src/urban_meal_delivery/db/restaurants.py b/src/urban_meal_delivery/db/restaurants.py new file mode 100644 index 0000000..4531d09 --- /dev/null +++ b/src/urban_meal_delivery/db/restaurants.py @@ -0,0 +1,42 @@ +"""Provide the ORM's Restaurant model.""" + +import sqlalchemy as sa +from sqlalchemy import orm + +from urban_meal_delivery.db import meta + + +class Restaurant(meta.Base): + """A Restaurant selling meals on the UDP.""" + + # pylint:disable=too-few-public-methods + + __tablename__ = 'restaurants' + + # Columns + id = sa.Column( # noqa:WPS125 + sa.SmallInteger, primary_key=True, autoincrement=False, + ) + created_at = sa.Column(sa.DateTime, nullable=False) + name = sa.Column(sa.Unicode(length=45), nullable=False) # noqa:WPS432 + _address_id = sa.Column('address_id', sa.Integer, nullable=False, index=True) + estimated_prep_duration = sa.Column(sa.SmallInteger, nullable=False) + + # Constraints + __table_args__ = ( + sa.ForeignKeyConstraint( + ['address_id'], ['addresses.id'], onupdate='RESTRICT', ondelete='RESTRICT', + ), + sa.CheckConstraint( + '0 <= estimated_prep_duration AND estimated_prep_duration <= 2400', + name='realistic_estimated_prep_duration', + ), + ) + + # Relationships + address = orm.relationship('Address', back_populates='restaurant') + orders = orm.relationship('Order', back_populates='restaurant') + + def __repr__(self) -> str: + """Non-literal text representation.""" + return '<{cls}({name})>'.format(cls=self.__class__.__name__, name=self.name) diff --git a/tests/db/__init__.py b/tests/db/__init__.py new file mode 100644 index 0000000..5f0df34 --- /dev/null +++ b/tests/db/__init__.py @@ -0,0 +1 @@ +"""Test the ORM layer.""" diff --git a/tests/db/conftest.py b/tests/db/conftest.py new file mode 100644 index 0000000..eeca169 --- /dev/null +++ b/tests/db/conftest.py @@ -0,0 +1,244 @@ +"""Utils for testing the ORM layer.""" + +import datetime + +import pytest +from sqlalchemy import schema + +from urban_meal_delivery import config +from urban_meal_delivery import db + + +@pytest.fixture(scope='session') +def db_engine(): + """Create all tables given the ORM models. + + The tables are put into a distinct PostgreSQL schema + that is removed after all tests are over. + + The engine used to do that is yielded. + """ + engine = db.make_engine() + engine.execute(schema.CreateSchema(config.CLEAN_SCHEMA)) + db.Base.metadata.create_all(engine) + + try: + yield engine + + finally: + engine.execute(schema.DropSchema(config.CLEAN_SCHEMA, cascade=True)) + + +@pytest.fixture +def db_session(db_engine): + """A SQLAlchemy session that rolls back everything after a test case.""" + connection = db_engine.connect() + # Begin the outer most transaction + # that is rolled back at the end of the test. + transaction = connection.begin() + # Create a session bound on the same connection as the transaction. + # Using any other session would not work. + Session = db.make_session_factory() # noqa:N806 + session = Session(bind=connection) + + try: + yield session + + finally: + session.close() + transaction.rollback() + connection.close() + + +@pytest.fixture +def address_data(): + """The data for an Address object in Paris.""" + return { + 'id': 1, + '_primary_id': 1, # => "itself" + 'created_at': datetime.datetime(2020, 1, 2, 3, 4, 5), + 'place_id': 'ChIJxSr71vZt5kcRoFHY4caCCxw', + 'latitude': 48.85313, + 'longitude': 2.37461, + '_city_id': 1, + 'city_name': 'St. German', + 'zip_code': '75011', + 'street': '42 Rue De Charonne', + 'floor': None, + } + + +@pytest.fixture +def address(address_data, city): + """An Address object.""" + address = db.Address(**address_data) + address.city = city + return address + + +@pytest.fixture +def address2_data(): + """The data for an Address object in Paris.""" + return { + 'id': 2, + '_primary_id': 2, # => "itself" + 'created_at': datetime.datetime(2020, 1, 2, 4, 5, 6), + 'place_id': 'ChIJs-9a6QZy5kcRY8Wwk9Ywzl8', + 'latitude': 48.852196, + 'longitude': 2.373937, + '_city_id': 1, + 'city_name': 'Paris', + 'zip_code': '75011', + 'street': 'Rue De Charonne 3', + 'floor': 2, + } + + +@pytest.fixture +def address2(address2_data, city): + """An Address object.""" + address2 = db.Address(**address2_data) + address2.city = city + return address2 + + +@pytest.fixture +def city_data(): + """The data for the City object modeling Paris.""" + return { + 'id': 1, + 'name': 'Paris', + 'kml': " ...", + '_center_latitude': 48.856614, + '_center_longitude': 2.3522219, + '_northeast_latitude': 48.9021449, + '_northeast_longitude': 2.4699208, + '_southwest_latitude': 48.815573, + '_southwest_longitude': 2.225193, + 'initial_zoom': 12, + } + + +@pytest.fixture +def city(city_data): + """A City object.""" + return db.City(**city_data) + + +@pytest.fixture +def courier_data(): + """The data for a Courier object.""" + return { + 'id': 1, + 'created_at': datetime.datetime(2020, 1, 2, 3, 4, 5), + 'vehicle': 'bicycle', + 'historic_speed': 7.89, + 'capacity': 100, + 'pay_per_hour': 750, + 'pay_per_order': 200, + } + + +@pytest.fixture +def courier(courier_data): + """A Courier object.""" + return db.Courier(**courier_data) + + +@pytest.fixture +def customer_data(): + """The data for the Customer object.""" + return {'id': 1} + + +@pytest.fixture +def customer(customer_data): + """A Customer object.""" + return db.Customer(**customer_data) + + +@pytest.fixture +def order_data(): + """The data for an ad-hoc Order object.""" + return { + 'id': 1, + '_delivery_id': 1, + '_customer_id': 1, + 'placed_at': datetime.datetime(2020, 1, 2, 11, 55, 11), + 'ad_hoc': True, + 'scheduled_delivery_at': None, + 'scheduled_delivery_at_corrected': None, + 'first_estimated_delivery_at': datetime.datetime(2020, 1, 2, 12, 35, 0), + 'cancelled': False, + 'cancelled_at': None, + 'cancelled_at_corrected': None, + 'sub_total': 2000, + 'delivery_fee': 250, + 'total': 2250, + '_restaurant_id': 1, + 'restaurant_notified_at': datetime.datetime(2020, 1, 2, 12, 5, 5), + 'restaurant_notified_at_corrected': False, + 'restaurant_confirmed_at': datetime.datetime(2020, 1, 2, 12, 5, 25), + 'restaurant_confirmed_at_corrected': False, + 'estimated_prep_duration': 900, + 'estimated_prep_duration_corrected': False, + 'estimated_prep_buffer': 480, + '_courier_id': 1, + 'dispatch_at': datetime.datetime(2020, 1, 2, 12, 5, 1), + 'dispatch_at_corrected': False, + 'courier_notified_at': datetime.datetime(2020, 1, 2, 12, 6, 2), + 'courier_notified_at_corrected': False, + 'courier_accepted_at': datetime.datetime(2020, 1, 2, 12, 6, 17), + 'courier_accepted_at_corrected': False, + 'utilization': 50, + '_pickup_address_id': 1, + 'reached_pickup_at': datetime.datetime(2020, 1, 2, 12, 16, 21), + 'pickup_at': datetime.datetime(2020, 1, 2, 12, 18, 1), + 'pickup_at_corrected': False, + 'pickup_not_confirmed': False, + 'left_pickup_at': datetime.datetime(2020, 1, 2, 12, 19, 45), + 'left_pickup_at_corrected': False, + '_delivery_address_id': 2, + 'reached_delivery_at': datetime.datetime(2020, 1, 2, 12, 27, 33), + 'delivery_at': datetime.datetime(2020, 1, 2, 12, 29, 55), + 'delivery_at_corrected': False, + 'delivery_not_confirmed': False, + '_courier_waited_at_delivery': False, + 'logged_delivery_distance': 500, + 'logged_avg_speed': 7.89, + 'logged_avg_speed_distance': 490, + } + + +@pytest.fixture +def order( # noqa:WPS211 pylint:disable=too-many-arguments + order_data, customer, restaurant, courier, address, address2, +): + """An Order object.""" + order = db.Order(**order_data) + order.customer = customer + order.restaurant = restaurant + order.courier = courier + order.pickup_address = address + order.delivery_address = address2 + return order + + +@pytest.fixture +def restaurant_data(): + """The data for the Restaurant object.""" + return { + 'id': 1, + 'created_at': datetime.datetime(2020, 1, 2, 3, 4, 5), + 'name': 'Vevay', + '_address_id': 1, + 'estimated_prep_duration': 1000, + } + + +@pytest.fixture +def restaurant(restaurant_data, address): + """A Restaurant object.""" + restaurant = db.Restaurant(**restaurant_data) + restaurant.address = address + return restaurant diff --git a/tests/db/test_addresses.py b/tests/db/test_addresses.py new file mode 100644 index 0000000..ffb5618 --- /dev/null +++ b/tests/db/test_addresses.py @@ -0,0 +1,141 @@ +"""Test the ORM's Address model.""" + +import pytest +from sqlalchemy import exc as sa_exc +from sqlalchemy.orm import exc as orm_exc + +from urban_meal_delivery import db + + +class TestSpecialMethods: + """Test special methods in Address.""" + + # pylint:disable=no-self-use + + def test_create_address(self, address_data): + """Test instantiation of a new Address object.""" + result = db.Address(**address_data) + + assert result is not None + + def test_text_representation(self, address_data): + """Address has a non-literal text representation.""" + address = db.Address(**address_data) + street = address_data['street'] + city_name = address_data['city_name'] + + result = repr(address) + + assert result == f'' + + +@pytest.mark.e2e +@pytest.mark.no_cover +class TestConstraints: + """Test the database constraints defined in Address.""" + + # pylint:disable=no-self-use + + def test_insert_into_database(self, address, db_session): + """Insert an instance into the database.""" + db_session.add(address) + db_session.commit() + + def test_dublicate_primary_key(self, address, address_data, city, db_session): + """Can only add a record once.""" + db_session.add(address) + db_session.commit() + + another_address = db.Address(**address_data) + another_address.city = city + db_session.add(another_address) + + with pytest.raises(orm_exc.FlushError): + db_session.commit() + + def test_delete_a_referenced_address(self, address, address_data, db_session): + """Remove a record that is referenced with a FK.""" + db_session.add(address) + db_session.commit() + + # Fake a second address that belongs to the same primary address. + address_data['id'] += 1 + another_address = db.Address(**address_data) + db_session.add(another_address) + db_session.commit() + + with pytest.raises(sa_exc.IntegrityError): + db_session.execute( + db.Address.__table__.delete().where( # noqa:WPS609 + db.Address.id == address.id, + ), + ) + + def test_delete_a_referenced_city(self, address, city, db_session): + """Remove a record that is referenced with a FK.""" + db_session.add(address) + db_session.commit() + + with pytest.raises(sa_exc.IntegrityError): + db_session.execute( + db.City.__table__.delete().where(db.City.id == city.id), # noqa:WPS609 + ) + + @pytest.mark.parametrize('latitude', [-91, 91]) + def test_invalid_latitude(self, address, db_session, latitude): + """Insert an instance with invalid data.""" + address.latitude = latitude + db_session.add(address) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + @pytest.mark.parametrize('longitude', [-181, 181]) + def test_invalid_longitude(self, address, db_session, longitude): + """Insert an instance with invalid data.""" + address.longitude = longitude + db_session.add(address) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + @pytest.mark.parametrize('zip_code', [-1, 0, 9999, 100000]) + def test_invalid_zip_code(self, address, db_session, zip_code): + """Insert an instance with invalid data.""" + address.zip_code = zip_code + db_session.add(address) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + @pytest.mark.parametrize('floor', [-1, 41]) + def test_invalid_floor(self, address, db_session, floor): + """Insert an instance with invalid data.""" + address.floor = floor + db_session.add(address) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + +class TestProperties: + """Test properties in Address.""" + + # pylint:disable=no-self-use + + def test_is_primary(self, address_data): + """Test Address.is_primary property.""" + address = db.Address(**address_data) + + result = address.is_primary + + assert result is True + + def test_is_not_primary(self, address_data): + """Test Address.is_primary property.""" + address_data['_primary_id'] = 999 + address = db.Address(**address_data) + + result = address.is_primary + + assert result is False diff --git a/tests/db/test_cities.py b/tests/db/test_cities.py new file mode 100644 index 0000000..50a7ecb --- /dev/null +++ b/tests/db/test_cities.py @@ -0,0 +1,99 @@ +"""Test the ORM's City model.""" + +import pytest +from sqlalchemy.orm import exc as orm_exc + +from urban_meal_delivery import db + + +class TestSpecialMethods: + """Test special methods in City.""" + + # pylint:disable=no-self-use + + def test_create_city(self, city_data): + """Test instantiation of a new City object.""" + result = db.City(**city_data) + + assert result is not None + + def test_text_representation(self, city_data): + """City has a non-literal text representation.""" + city = db.City(**city_data) + name = city_data['name'] + + result = repr(city) + + assert result == f'' + + +@pytest.mark.e2e +@pytest.mark.no_cover +class TestConstraints: + """Test the database constraints defined in City.""" + + # pylint:disable=no-self-use + + def test_insert_into_database(self, city, db_session): + """Insert an instance into the database.""" + db_session.add(city) + db_session.commit() + + def test_dublicate_primary_key(self, city, city_data, db_session): + """Can only add a record once.""" + db_session.add(city) + db_session.commit() + + another_city = db.City(**city_data) + db_session.add(another_city) + + with pytest.raises(orm_exc.FlushError): + db_session.commit() + + +class TestProperties: + """Test properties in City.""" + + # pylint:disable=no-self-use + + def test_location_data(self, city_data): + """Test City.location property.""" + city = db.City(**city_data) + + result = city.location + + assert isinstance(result, dict) + assert len(result) == 2 + assert result['latitude'] == pytest.approx(city_data['_center_latitude']) + assert result['longitude'] == pytest.approx(city_data['_center_longitude']) + + def test_viewport_data_overall(self, city_data): + """Test City.viewport property.""" + city = db.City(**city_data) + + result = city.viewport + + assert isinstance(result, dict) + assert len(result) == 2 + + def test_viewport_data_northeast(self, city_data): + """Test City.viewport property.""" + city = db.City(**city_data) + + result = city.viewport['northeast'] + + assert isinstance(result, dict) + assert len(result) == 2 + assert result['latitude'] == pytest.approx(city_data['_northeast_latitude']) + assert result['longitude'] == pytest.approx(city_data['_northeast_longitude']) + + def test_viewport_data_southwest(self, city_data): + """Test City.viewport property.""" + city = db.City(**city_data) + + result = city.viewport['southwest'] + + assert isinstance(result, dict) + assert len(result) == 2 + assert result['latitude'] == pytest.approx(city_data['_southwest_latitude']) + assert result['longitude'] == pytest.approx(city_data['_southwest_longitude']) diff --git a/tests/db/test_couriers.py b/tests/db/test_couriers.py new file mode 100644 index 0000000..a3ba103 --- /dev/null +++ b/tests/db/test_couriers.py @@ -0,0 +1,125 @@ +"""Test the ORM's Courier model.""" + +import pytest +from sqlalchemy import exc as sa_exc +from sqlalchemy.orm import exc as orm_exc + +from urban_meal_delivery import db + + +class TestSpecialMethods: + """Test special methods in Courier.""" + + # pylint:disable=no-self-use + + def test_create_courier(self, courier_data): + """Test instantiation of a new Courier object.""" + result = db.Courier(**courier_data) + + assert result is not None + + def test_text_representation(self, courier_data): + """Courier has a non-literal text representation.""" + courier_data['id'] = 1 + courier = db.Courier(**courier_data) + id_ = courier_data['id'] + + result = repr(courier) + + assert result == f'' + + +@pytest.mark.e2e +@pytest.mark.no_cover +class TestConstraints: + """Test the database constraints defined in Courier.""" + + # pylint:disable=no-self-use + + def test_insert_into_database(self, courier, db_session): + """Insert an instance into the database.""" + db_session.add(courier) + db_session.commit() + + def test_dublicate_primary_key(self, courier, courier_data, db_session): + """Can only add a record once.""" + db_session.add(courier) + db_session.commit() + + another_courier = db.Courier(**courier_data) + db_session.add(another_courier) + + with pytest.raises(orm_exc.FlushError): + db_session.commit() + + def test_invalid_vehicle(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.vehicle = 'invalid' + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_negative_speed(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.historic_speed = -1 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_unrealistic_speed(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.historic_speed = 999 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_negative_capacity(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.capacity = -1 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_too_much_capacity(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.capacity = 999 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_negative_pay_per_hour(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.pay_per_hour = -1 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_too_much_pay_per_hour(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.pay_per_hour = 9999 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_negative_pay_per_order(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.pay_per_order = -1 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_too_much_pay_per_order(self, courier, db_session): + """Insert an instance with invalid data.""" + courier.pay_per_order = 999 + db_session.add(courier) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() diff --git a/tests/db/test_customer.py b/tests/db/test_customer.py new file mode 100644 index 0000000..487a11c --- /dev/null +++ b/tests/db/test_customer.py @@ -0,0 +1,51 @@ +"""Test the ORM's Customer model.""" + +import pytest +from sqlalchemy.orm import exc as orm_exc + +from urban_meal_delivery import db + + +class TestSpecialMethods: + """Test special methods in Customer.""" + + # pylint:disable=no-self-use + + def test_create_customer(self, customer_data): + """Test instantiation of a new Customer object.""" + result = db.Customer(**customer_data) + + assert result is not None + + def test_text_representation(self, customer_data): + """Customer has a non-literal text representation.""" + customer = db.Customer(**customer_data) + id_ = customer_data['id'] + + result = repr(customer) + + assert result == f'' + + +@pytest.mark.e2e +@pytest.mark.no_cover +class TestConstraints: + """Test the database constraints defined in Customer.""" + + # pylint:disable=no-self-use + + def test_insert_into_database(self, customer, db_session): + """Insert an instance into the database.""" + db_session.add(customer) + db_session.commit() + + def test_dublicate_primary_key(self, customer, customer_data, db_session): + """Can only add a record once.""" + db_session.add(customer) + db_session.commit() + + another_customer = db.Customer(**customer_data) + db_session.add(another_customer) + + with pytest.raises(orm_exc.FlushError): + db_session.commit() diff --git a/tests/db/test_orders.py b/tests/db/test_orders.py new file mode 100644 index 0000000..fa36072 --- /dev/null +++ b/tests/db/test_orders.py @@ -0,0 +1,397 @@ +"""Test the ORM's Order model.""" + +import datetime + +import pytest +from sqlalchemy.orm import exc as orm_exc + +from urban_meal_delivery import db + + +class TestSpecialMethods: + """Test special methods in Order.""" + + # pylint:disable=no-self-use + + def test_create_order(self, order_data): + """Test instantiation of a new Order object.""" + result = db.Order(**order_data) + + assert result is not None + + def test_text_representation(self, order_data): + """Order has a non-literal text representation.""" + order = db.Order(**order_data) + id_ = order_data['id'] + + result = repr(order) + + assert result == f'' + + +@pytest.mark.e2e +@pytest.mark.no_cover +class TestConstraints: + """Test the database constraints defined in Order.""" + + # pylint:disable=no-self-use + + def test_insert_into_database(self, order, db_session): + """Insert an instance into the database.""" + db_session.add(order) + db_session.commit() + + def test_dublicate_primary_key(self, order, order_data, city, db_session): + """Can only add a record once.""" + db_session.add(order) + db_session.commit() + + another_order = db.Order(**order_data) + another_order.city = city + db_session.add(another_order) + + with pytest.raises(orm_exc.FlushError): + db_session.commit() + + # TODO (order-constraints): the various Foreign Key and Check Constraints + # should be tested eventually. This is not of highest importance as + # we have a lot of confidence from the data cleaning notebook. + + +class TestProperties: + """Test properties in Order.""" + + # pylint:disable=no-self-use,too-many-public-methods + + def test_is_not_scheduled(self, order_data): + """Test Order.scheduled property.""" + order = db.Order(**order_data) + + result = order.scheduled + + assert result is False + + def test_is_scheduled(self, order_data): + """Test Order.scheduled property.""" + order_data['ad_hoc'] = False + order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0) + order_data['scheduled_delivery_at_corrected'] = False + order = db.Order(**order_data) + + result = order.scheduled + + assert result is True + + def test_is_completed(self, order_data): + """Test Order.completed property.""" + order = db.Order(**order_data) + + result = order.completed + + assert result is True + + def test_is_not_completed(self, order_data): + """Test Order.completed property.""" + order_data['cancelled'] = True + order_data['cancelled_at'] = datetime.datetime(2020, 1, 2, 12, 15, 0) + order_data['cancelled_at_corrected'] = False + order = db.Order(**order_data) + + result = order.completed + + assert result is False + + def test_is_corrected(self, order_data): + """Test Order.corrected property.""" + order_data['dispatch_at_corrected'] = True + order = db.Order(**order_data) + + result = order.corrected + + assert result is True + + def test_time_to_accept_no_dispatch_at(self, order_data): + """Test Order.time_to_accept property.""" + order_data['dispatch_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_accept) + + def test_time_to_accept_no_courier_accepted(self, order_data): + """Test Order.time_to_accept property.""" + order_data['courier_accepted_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_accept) + + def test_time_to_accept_success(self, order_data): + """Test Order.time_to_accept property.""" + order = db.Order(**order_data) + + result = order.time_to_accept + + assert isinstance(result, datetime.timedelta) + + def test_time_to_react_no_courier_notified(self, order_data): + """Test Order.time_to_react property.""" + order_data['courier_notified_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_react) + + def test_time_to_react_no_courier_accepted(self, order_data): + """Test Order.time_to_react property.""" + order_data['courier_accepted_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_react) + + def test_time_to_react_success(self, order_data): + """Test Order.time_to_react property.""" + order = db.Order(**order_data) + + result = order.time_to_react + + assert isinstance(result, datetime.timedelta) + + def test_time_to_pickup_no_reached_pickup_at(self, order_data): + """Test Order.time_to_pickup property.""" + order_data['reached_pickup_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_pickup) + + def test_time_to_pickup_no_courier_accepted(self, order_data): + """Test Order.time_to_pickup property.""" + order_data['courier_accepted_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_pickup) + + def test_time_to_pickup_success(self, order_data): + """Test Order.time_to_pickup property.""" + order = db.Order(**order_data) + + result = order.time_to_pickup + + assert isinstance(result, datetime.timedelta) + + def test_time_at_pickup_no_reached_pickup_at(self, order_data): + """Test Order.time_at_pickup property.""" + order_data['reached_pickup_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_at_pickup) + + def test_time_at_pickup_no_pickup_at(self, order_data): + """Test Order.time_at_pickup property.""" + order_data['pickup_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_at_pickup) + + def test_time_at_pickup_success(self, order_data): + """Test Order.time_at_pickup property.""" + order = db.Order(**order_data) + + result = order.time_at_pickup + + assert isinstance(result, datetime.timedelta) + + def test_scheduled_pickup_at_no_restaurant_notified( # noqa:WPS118 + self, order_data, + ): + """Test Order.scheduled_pickup_at property.""" + order_data['restaurant_notified_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.scheduled_pickup_at) + + def test_scheduled_pickup_at_no_est_prep_duration(self, order_data): # noqa:WPS118 + """Test Order.scheduled_pickup_at property.""" + order_data['estimated_prep_duration'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.scheduled_pickup_at) + + def test_scheduled_pickup_at_success(self, order_data): + """Test Order.scheduled_pickup_at property.""" + order = db.Order(**order_data) + + result = order.scheduled_pickup_at + + assert isinstance(result, datetime.datetime) + + def test_if_courier_early_at_pickup(self, order_data): + """Test Order.courier_early property.""" + order = db.Order(**order_data) + + result = order.courier_early + + assert bool(result) is True + + def test_if_courier_late_at_pickup(self, order_data): + """Test Order.courier_late property.""" + # Opposite of test case before. + order = db.Order(**order_data) + + result = order.courier_late + + assert bool(result) is False + + def test_if_restaurant_early_at_pickup(self, order_data): + """Test Order.restaurant_early property.""" + order = db.Order(**order_data) + + result = order.restaurant_early + + assert bool(result) is True + + def test_if_restaurant_late_at_pickup(self, order_data): + """Test Order.restaurant_late property.""" + # Opposite of test case before. + order = db.Order(**order_data) + + result = order.restaurant_late + + assert bool(result) is False + + def test_time_to_delivery_no_reached_delivery_at(self, order_data): # noqa:WPS118 + """Test Order.time_to_delivery property.""" + order_data['reached_delivery_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_delivery) + + def test_time_to_delivery_no_pickup_at(self, order_data): + """Test Order.time_to_delivery property.""" + order_data['pickup_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_to_delivery) + + def test_time_to_delivery_success(self, order_data): + """Test Order.time_to_delivery property.""" + order = db.Order(**order_data) + + result = order.time_to_delivery + + assert isinstance(result, datetime.timedelta) + + def test_time_at_delivery_no_reached_delivery_at(self, order_data): # noqa:WPS118 + """Test Order.time_at_delivery property.""" + order_data['reached_delivery_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_at_delivery) + + def test_time_at_delivery_no_delivery_at(self, order_data): + """Test Order.time_at_delivery property.""" + order_data['delivery_at'] = None + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='not set'): + int(order.time_at_delivery) + + def test_time_at_delivery_success(self, order_data): + """Test Order.time_at_delivery property.""" + order = db.Order(**order_data) + + result = order.time_at_delivery + + assert isinstance(result, datetime.timedelta) + + def test_courier_waited_at_delviery(self, order_data): + """Test Order.courier_waited_at_delivery property.""" + order_data['_courier_waited_at_delivery'] = True + order = db.Order(**order_data) + + result = int(order.courier_waited_at_delivery.total_seconds()) + + assert result > 0 + + def test_courier_did_not_wait_at_delivery(self, order_data): + """Test Order.courier_waited_at_delivery property.""" + order_data['_courier_waited_at_delivery'] = False + order = db.Order(**order_data) + + result = int(order.courier_waited_at_delivery.total_seconds()) + + assert result == 0 + + def test_if_delivery_early_success(self, order_data): + """Test Order.delivery_early property.""" + order_data['ad_hoc'] = False + order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0) + order_data['scheduled_delivery_at_corrected'] = False + order = db.Order(**order_data) + + result = order.delivery_early + + assert bool(result) is True + + def test_if_delivery_early_failure(self, order_data): + """Test Order.delivery_early property.""" + order = db.Order(**order_data) + + with pytest.raises(AttributeError, match='scheduled'): + int(order.delivery_early) + + def test_if_delivery_late_success(self, order_data): + """Test Order.delivery_late property.""" + order_data['ad_hoc'] = False + order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0) + order_data['scheduled_delivery_at_corrected'] = False + order = db.Order(**order_data) + + result = order.delivery_late + + assert bool(result) is False + + def test_if_delivery_late_failure(self, order_data): + """Test Order.delivery_late property.""" + order = db.Order(**order_data) + + with pytest.raises(AttributeError, match='scheduled'): + int(order.delivery_late) + + def test_no_total_time_for_pre_order(self, order_data): + """Test Order.total_time property.""" + order_data['ad_hoc'] = False + order_data['scheduled_delivery_at'] = datetime.datetime(2020, 1, 2, 12, 30, 0) + order_data['scheduled_delivery_at_corrected'] = False + order = db.Order(**order_data) + + with pytest.raises(AttributeError, match='Scheduled'): + int(order.total_time) + + def test_no_total_time_for_cancelled_order(self, order_data): + """Test Order.total_time property.""" + order_data['cancelled'] = True + order_data['cancelled_at'] = datetime.datetime(2020, 1, 2, 12, 15, 0) + order_data['cancelled_at_corrected'] = False + order = db.Order(**order_data) + + with pytest.raises(RuntimeError, match='Cancelled'): + int(order.total_time) + + def test_total_time_success(self, order_data): + """Test Order.total_time property.""" + order = db.Order(**order_data) + + result = order.total_time + + assert isinstance(result, datetime.timedelta) diff --git a/tests/db/test_restaurants.py b/tests/db/test_restaurants.py new file mode 100644 index 0000000..4662346 --- /dev/null +++ b/tests/db/test_restaurants.py @@ -0,0 +1,80 @@ +"""Test the ORM's Restaurant model.""" + +import pytest +from sqlalchemy import exc as sa_exc +from sqlalchemy.orm import exc as orm_exc + +from urban_meal_delivery import db + + +class TestSpecialMethods: + """Test special methods in Restaurant.""" + + # pylint:disable=no-self-use + + def test_create_restaurant(self, restaurant_data): + """Test instantiation of a new Restaurant object.""" + result = db.Restaurant(**restaurant_data) + + assert result is not None + + def test_text_representation(self, restaurant_data): + """Restaurant has a non-literal text representation.""" + restaurant = db.Restaurant(**restaurant_data) + name = restaurant_data['name'] + + result = repr(restaurant) + + assert result == f'' + + +@pytest.mark.e2e +@pytest.mark.no_cover +class TestConstraints: + """Test the database constraints defined in Restaurant.""" + + # pylint:disable=no-self-use + + def test_insert_into_database(self, restaurant, db_session): + """Insert an instance into the database.""" + db_session.add(restaurant) + db_session.commit() + + def test_dublicate_primary_key(self, restaurant, restaurant_data, db_session): + """Can only add a record once.""" + db_session.add(restaurant) + db_session.commit() + + another_restaurant = db.Restaurant(**restaurant_data) + db_session.add(another_restaurant) + + with pytest.raises(orm_exc.FlushError): + db_session.commit() + + def test_delete_a_referenced_address(self, restaurant, address, db_session): + """Remove a record that is referenced with a FK.""" + db_session.add(restaurant) + db_session.commit() + + with pytest.raises(sa_exc.IntegrityError): + db_session.execute( + db.Address.__table__.delete().where( # noqa:WPS609 + db.Address.id == address.id, + ), + ) + + def test_negative_prep_duration(self, restaurant, db_session): + """Insert an instance with invalid data.""" + restaurant.estimated_prep_duration = -1 + db_session.add(restaurant) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() + + def test_too_high_prep_duration(self, restaurant, db_session): + """Insert an instance with invalid data.""" + restaurant.estimated_prep_duration = 2500 + db_session.add(restaurant) + + with pytest.raises(sa_exc.IntegrityError): + db_session.commit() diff --git a/tests/test_config.py b/tests/test_config.py index 8983808..04c79f5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -43,3 +43,11 @@ def test_no_database_uri_set(env, monkeypatch): with pytest.warns(UserWarning, match='no DATABASE_URI'): config_mod.get_config(env) + + +def test_random_testing_schema(): + """CLEAN_SCHEMA is randomized if not seti explicitly.""" + result = config_mod.random_schema_name() + + assert isinstance(result, str) + assert len(result) <= 10 From a16c260543798d91a7f377cf2d84a70075252248 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Sun, 9 Aug 2020 17:14:23 +0200 Subject: [PATCH 05/14] Add database migrations - use Alembic to migrate the PostgreSQL database + create initial migration script to set up the database, as an alternative to db.Base.metadata.create_all() + integrate Alembic into the test suite; the db_engine fixture now has two modes: * create the latest version of tables all at once * invoke `alembic upgrade head` => the "e2e" tests are all run twice, once in each mode; this ensures that the migration scripts re-create the same database schema as db.Base.metadata.create_all() would * in both modes, a temporary PostgreSQL schema is used to create the tables in => could now run "e2e" tests against production database and still have isolation - make the configuration module public (to be used by Alembic) - adjust linting rules for Alembic --- alembic.ini | 44 + migrations/README.md | 4 + migrations/env.py | 45 + migrations/script.py.mako | 31 + ...806_23_f11cd76d2f45_create_the_database.py | 802 ++++++++++++++++++ noxfile.py | 11 +- poetry.lock | 86 +- pyproject.toml | 2 + setup.cfg | 18 +- src/urban_meal_delivery/__init__.py | 6 +- .../{_config.py => configuration.py} | 22 +- tests/conftest.py | 12 + tests/db/conftest.py | 31 +- tests/test_config.py | 25 +- 14 files changed, 1104 insertions(+), 35 deletions(-) create mode 100644 alembic.ini create mode 100644 migrations/README.md create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/rev_20200806_23_f11cd76d2f45_create_the_database.py rename src/urban_meal_delivery/{_config.py => configuration.py} (80%) create mode 100644 tests/conftest.py diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..4aacbae --- /dev/null +++ b/alembic.ini @@ -0,0 +1,44 @@ +[alembic] +file_template = rev_%%(year)d%%(month).2d%%(day).2d_%%(hour).2d_%%(rev)s_%%(slug)s +script_location = %(here)s/migrations + +[post_write_hooks] +hooks=black +black.type=console_scripts +black.entrypoint=black + +# The following is taken from the default alembic.ini file. + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..243a2ee --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,4 @@ +# Database Migrations + +This project uses [alembic](https://alembic.sqlalchemy.org/en/latest) +to run the database migrations diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..15c79e3 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,45 @@ +"""Configure Alembic's migration environment.""" + +import os +from logging import config as log_config + +import sqlalchemy as sa +from alembic import context + +from urban_meal_delivery import config as umd_config +from urban_meal_delivery import db + + +# Disable the --sql option, a.k.a, the "offline mode". +if context.is_offline_mode(): + raise NotImplementedError('The --sql option is not implemented in this project') + + +# Set up the default Python logger from the alembic.ini file. +log_config.fileConfig(context.config.config_file_name) + + +def include_object(obj, _name, type_, _reflected, _compare_to): + """Only include the clean schema into --autogenerate migrations.""" + if type_ in {'table', 'column'} and obj.schema != umd_config.DATABASE_SCHEMA: + return False + + return True + + +engine = sa.create_engine(umd_config.DATABASE_URI) + +with engine.connect() as connection: + context.configure( + connection=connection, + include_object=include_object, + target_metadata=db.Base.metadata, + version_table='{alembic_table}{test_schema}'.format( + alembic_table=umd_config.ALEMBIC_TABLE, + test_schema=(f'_{umd_config.CLEAN_SCHEMA}' if os.getenv('TESTING') else ''), + ), + version_table_schema=umd_config.ALEMBIC_TABLE_SCHEMA, + ) + + with context.begin_transaction(): + context.run_migrations() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..1b03c53 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,31 @@ +"""${message}. + +Revision: # ${up_revision} at ${create_date} +Revises: # ${down_revision | comma,n} +""" + +import os + +import sqlalchemy as sa +from alembic import op +${imports if imports else ""} + +from urban_meal_delivery import configuration + +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +config = configuration.make_config('testing' if os.getenv('TESTING') else 'production') + + +def upgrade(): + """Upgrade to revision ${up_revision}.""" + ${upgrades if upgrades else "pass"} + + +def downgrade(): + """Downgrade to revision ${down_revision}.""" + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/rev_20200806_23_f11cd76d2f45_create_the_database.py b/migrations/versions/rev_20200806_23_f11cd76d2f45_create_the_database.py new file mode 100644 index 0000000..a03e1dc --- /dev/null +++ b/migrations/versions/rev_20200806_23_f11cd76d2f45_create_the_database.py @@ -0,0 +1,802 @@ +"""Create the database from scratch. + +Revision: #f11cd76d2f45 at 2020-08-06 23:24:32 +""" + +import os + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +from urban_meal_delivery import configuration + + +revision = 'f11cd76d2f45' +down_revision = None +branch_labels = None +depends_on = None + + +config = configuration.make_config('testing' if os.getenv('TESTING') else 'production') + + +def upgrade(): + """Upgrade to revision f11cd76d2f45.""" + op.execute(f'CREATE SCHEMA {config.CLEAN_SCHEMA};') + op.create_table( # noqa:ECE001 + 'cities', + sa.Column('id', sa.SmallInteger(), autoincrement=False, nullable=False), + sa.Column('name', sa.Unicode(length=10), nullable=False), + sa.Column('kml', sa.UnicodeText(), nullable=False), + sa.Column('center_latitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('center_longitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('northeast_latitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('northeast_longitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('southwest_latitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('southwest_longitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('initial_zoom', sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_cities')), + *( + [ # noqa:WPS504 + sa.ForeignKeyConstraint( + ['id'], + [f'{config.ORIGINAL_SCHEMA}.cities.id'], + name=op.f('pk_cities_sanity'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + ] + if not config.TESTING + else [] + ), + schema=config.CLEAN_SCHEMA, + ) + op.create_table( # noqa:ECE001 + 'couriers', + sa.Column('id', sa.Integer(), autoincrement=False, nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('vehicle', sa.Unicode(length=10), nullable=False), + sa.Column('speed', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('capacity', sa.SmallInteger(), nullable=False), + sa.Column('pay_per_hour', sa.SmallInteger(), nullable=False), + sa.Column('pay_per_order', sa.SmallInteger(), nullable=False), + sa.CheckConstraint( + "vehicle IN ('bicycle', 'motorcycle')", + name=op.f('ck_couriers_on_available_vehicle_types'), + ), + sa.CheckConstraint( + '0 <= capacity AND capacity <= 200', + name=op.f('ck_couriers_on_capacity_under_200_liters'), + ), + sa.CheckConstraint( + '0 <= pay_per_hour AND pay_per_hour <= 1500', + name=op.f('ck_couriers_on_realistic_pay_per_hour'), + ), + sa.CheckConstraint( + '0 <= pay_per_order AND pay_per_order <= 650', + name=op.f('ck_couriers_on_realistic_pay_per_order'), + ), + sa.CheckConstraint( + '0 <= speed AND speed <= 30', name=op.f('ck_couriers_on_realistic_speed'), + ), + sa.PrimaryKeyConstraint('id', name=op.f('pk_couriers')), + *( + [ # noqa:WPS504 + sa.ForeignKeyConstraint( + ['id'], + [f'{config.ORIGINAL_SCHEMA}.couriers.id'], + name=op.f('pk_couriers_sanity'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + ] + if not config.TESTING + else [] + ), + schema=config.CLEAN_SCHEMA, + ) + op.create_table( + 'customers', + sa.Column('id', sa.Integer(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_customers')), + schema=config.CLEAN_SCHEMA, + ) + op.create_table( # noqa:ECE001 + 'addresses', + sa.Column('id', sa.Integer(), autoincrement=False, nullable=False), + sa.Column('primary_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('place_id', sa.Unicode(length=120), nullable=False), # noqa:WPS432 + sa.Column('latitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('longitude', postgresql.DOUBLE_PRECISION(), nullable=False), + sa.Column('city_id', sa.SmallInteger(), nullable=False), + sa.Column('city', sa.Unicode(length=25), nullable=False), # noqa:WPS432 + sa.Column('zip_code', sa.Integer(), nullable=False), + sa.Column('street', sa.Unicode(length=80), nullable=False), # noqa:WPS432 + sa.Column('floor', sa.SmallInteger(), nullable=True), + sa.CheckConstraint( + '-180 <= longitude AND longitude <= 180', + name=op.f('ck_addresses_on_longitude_between_180_degrees'), + ), + sa.CheckConstraint( + '-90 <= latitude AND latitude <= 90', + name=op.f('ck_addresses_on_latitude_between_90_degrees'), + ), + sa.CheckConstraint( + '0 <= floor AND floor <= 40', name=op.f('ck_addresses_on_realistic_floor'), + ), + sa.CheckConstraint( + '30000 <= zip_code AND zip_code <= 99999', + name=op.f('ck_addresses_on_valid_zip_code'), + ), + sa.ForeignKeyConstraint( + ['city_id'], + [f'{config.CLEAN_SCHEMA}.cities.id'], + name=op.f('fk_addresses_to_cities_via_city_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['primary_id'], + [f'{config.CLEAN_SCHEMA}.addresses.id'], + name=op.f('fk_addresses_to_addresses_via_primary_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.PrimaryKeyConstraint('id', name=op.f('pk_addresses')), + *( + [ # noqa:WPS504 + sa.ForeignKeyConstraint( + ['id'], + [f'{config.ORIGINAL_SCHEMA}.addresses.id'], + name=op.f('pk_addresses_sanity'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + ] + if not config.TESTING + else [] + ), + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_addresses_on_city_id'), + 'addresses', + ['city_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_addresses_on_place_id'), + 'addresses', + ['place_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_addresses_on_primary_id'), + 'addresses', + ['primary_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_addresses_on_zip_code'), + 'addresses', + ['zip_code'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_table( # noqa:ECE001 + 'restaurants', + sa.Column('id', sa.SmallInteger(), autoincrement=False, nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('name', sa.Unicode(length=45), nullable=False), # noqa:WPS432 + sa.Column('address_id', sa.Integer(), nullable=False), + sa.Column('estimated_prep_duration', sa.SmallInteger(), nullable=False), + sa.CheckConstraint( + '0 <= estimated_prep_duration AND estimated_prep_duration <= 2400', + name=op.f('ck_restaurants_on_realistic_estimated_prep_duration'), + ), + sa.ForeignKeyConstraint( + ['address_id'], + [f'{config.CLEAN_SCHEMA}.addresses.id'], + name=op.f('fk_restaurants_to_addresses_via_address_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.PrimaryKeyConstraint('id', name=op.f('pk_restaurants')), + *( + [ # noqa:WPS504 + sa.ForeignKeyConstraint( + ['id'], + [f'{config.ORIGINAL_SCHEMA}.businesses.id'], + name=op.f('pk_restaurants_sanity'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + ] + if not config.TESTING + else [] + ), + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_restaurants_on_address_id'), + 'restaurants', + ['address_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_table( # noqa:ECE001 + 'orders', + sa.Column('id', sa.Integer(), autoincrement=False, nullable=False), + sa.Column('delivery_id', sa.Integer(), nullable=True), + sa.Column('customer_id', sa.Integer(), nullable=False), + sa.Column('placed_at', sa.DateTime(), nullable=False), + sa.Column('ad_hoc', sa.Boolean(), nullable=False), + sa.Column('scheduled_delivery_at', sa.DateTime(), nullable=True), + sa.Column('scheduled_delivery_at_corrected', sa.Boolean(), nullable=True), + sa.Column('first_estimated_delivery_at', sa.DateTime(), nullable=True), + sa.Column('cancelled', sa.Boolean(), nullable=False), + sa.Column('cancelled_at', sa.DateTime(), nullable=True), + sa.Column('cancelled_at_corrected', sa.Boolean(), nullable=True), + sa.Column('sub_total', sa.Integer(), nullable=False), + sa.Column('delivery_fee', sa.SmallInteger(), nullable=False), + sa.Column('total', sa.Integer(), nullable=False), + sa.Column('restaurant_id', sa.SmallInteger(), nullable=False), + sa.Column('restaurant_notified_at', sa.DateTime(), nullable=True), + sa.Column('restaurant_notified_at_corrected', sa.Boolean(), nullable=True), + sa.Column('restaurant_confirmed_at', sa.DateTime(), nullable=True), + sa.Column('restaurant_confirmed_at_corrected', sa.Boolean(), nullable=True), + sa.Column('estimated_prep_duration', sa.Integer(), nullable=True), + sa.Column('estimated_prep_duration_corrected', sa.Boolean(), nullable=True), + sa.Column('estimated_prep_buffer', sa.Integer(), nullable=False), + sa.Column('courier_id', sa.Integer(), nullable=True), + sa.Column('dispatch_at', sa.DateTime(), nullable=True), + sa.Column('dispatch_at_corrected', sa.Boolean(), nullable=True), + sa.Column('courier_notified_at', sa.DateTime(), nullable=True), + sa.Column('courier_notified_at_corrected', sa.Boolean(), nullable=True), + sa.Column('courier_accepted_at', sa.DateTime(), nullable=True), + sa.Column('courier_accepted_at_corrected', sa.Boolean(), nullable=True), + sa.Column('utilization', sa.SmallInteger(), nullable=False), + sa.Column('pickup_address_id', sa.Integer(), nullable=False), + sa.Column('reached_pickup_at', sa.DateTime(), nullable=True), + sa.Column('pickup_at', sa.DateTime(), nullable=True), + sa.Column('pickup_at_corrected', sa.Boolean(), nullable=True), + sa.Column('pickup_not_confirmed', sa.Boolean(), nullable=True), + sa.Column('left_pickup_at', sa.DateTime(), nullable=True), + sa.Column('left_pickup_at_corrected', sa.Boolean(), nullable=True), + sa.Column('delivery_address_id', sa.Integer(), nullable=False), + sa.Column('reached_delivery_at', sa.DateTime(), nullable=True), + sa.Column('delivery_at', sa.DateTime(), nullable=True), + sa.Column('delivery_at_corrected', sa.Boolean(), nullable=True), + sa.Column('delivery_not_confirmed', sa.Boolean(), nullable=True), + sa.Column('courier_waited_at_delivery', sa.Boolean(), nullable=True), + sa.Column('logged_delivery_distance', sa.SmallInteger(), nullable=True), + sa.Column('logged_avg_speed', postgresql.DOUBLE_PRECISION(), nullable=True), + sa.Column('logged_avg_speed_distance', sa.SmallInteger(), nullable=True), + sa.CheckConstraint( + '0 <= estimated_prep_buffer AND estimated_prep_buffer <= 900', + name=op.f('ck_orders_on_estimated_prep_buffer_between_0_and_900'), + ), + sa.CheckConstraint( + '0 <= estimated_prep_duration AND estimated_prep_duration <= 2700', + name=op.f('ck_orders_on_estimated_prep_duration_between_0_and_2700'), + ), + sa.CheckConstraint( + '0 <= utilization AND utilization <= 100', + name=op.f('ck_orders_on_utilization_between_0_and_100'), + ), + sa.CheckConstraint( + '(cancelled_at IS NULL AND cancelled_at_corrected IS NULL) OR (cancelled_at IS NULL AND cancelled_at_corrected IS TRUE) OR (cancelled_at IS NOT NULL AND cancelled_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_1'), + ), + sa.CheckConstraint( + '(courier_accepted_at IS NULL AND courier_accepted_at_corrected IS NULL) OR (courier_accepted_at IS NULL AND courier_accepted_at_corrected IS TRUE) OR (courier_accepted_at IS NOT NULL AND courier_accepted_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_7'), + ), + sa.CheckConstraint( + '(courier_notified_at IS NULL AND courier_notified_at_corrected IS NULL) OR (courier_notified_at IS NULL AND courier_notified_at_corrected IS TRUE) OR (courier_notified_at IS NOT NULL AND courier_notified_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_6'), + ), + sa.CheckConstraint( + '(delivery_at IS NULL AND delivery_at_corrected IS NULL) OR (delivery_at IS NULL AND delivery_at_corrected IS TRUE) OR (delivery_at IS NOT NULL AND delivery_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_10'), + ), + sa.CheckConstraint( + '(dispatch_at IS NULL AND dispatch_at_corrected IS NULL) OR (dispatch_at IS NULL AND dispatch_at_corrected IS TRUE) OR (dispatch_at IS NOT NULL AND dispatch_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_5'), + ), + sa.CheckConstraint( + '(estimated_prep_duration IS NULL AND estimated_prep_duration_corrected IS NULL) OR (estimated_prep_duration IS NULL AND estimated_prep_duration_corrected IS TRUE) OR (estimated_prep_duration IS NOT NULL AND estimated_prep_duration_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_4'), + ), + sa.CheckConstraint( + '(left_pickup_at IS NULL AND left_pickup_at_corrected IS NULL) OR (left_pickup_at IS NULL AND left_pickup_at_corrected IS TRUE) OR (left_pickup_at IS NOT NULL AND left_pickup_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_9'), + ), + sa.CheckConstraint( + '(pickup_at IS NULL AND pickup_at_corrected IS NULL) OR (pickup_at IS NULL AND pickup_at_corrected IS TRUE) OR (pickup_at IS NOT NULL AND pickup_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_8'), + ), + sa.CheckConstraint( + '(restaurant_confirmed_at IS NULL AND restaurant_confirmed_at_corrected IS NULL) OR (restaurant_confirmed_at IS NULL AND restaurant_confirmed_at_corrected IS TRUE) OR (restaurant_confirmed_at IS NOT NULL AND restaurant_confirmed_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_3'), + ), + sa.CheckConstraint( + '(restaurant_notified_at IS NULL AND restaurant_notified_at_corrected IS NULL) OR (restaurant_notified_at IS NULL AND restaurant_notified_at_corrected IS TRUE) OR (restaurant_notified_at IS NOT NULL AND restaurant_notified_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_2'), + ), + sa.CheckConstraint( + '(scheduled_delivery_at IS NULL AND scheduled_delivery_at_corrected IS NULL) OR (scheduled_delivery_at IS NULL AND scheduled_delivery_at_corrected IS TRUE) OR (scheduled_delivery_at IS NOT NULL AND scheduled_delivery_at_corrected IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_corrections_only_for_set_value_0'), + ), + sa.CheckConstraint( + '(ad_hoc IS TRUE AND scheduled_delivery_at IS NULL) OR (ad_hoc IS FALSE AND scheduled_delivery_at IS NOT NULL)', # noqa:E501 + name=op.f('ck_orders_on_either_ad_hoc_or_scheduled_order'), + ), + sa.CheckConstraint( + 'NOT (EXTRACT(EPOCH FROM scheduled_delivery_at - placed_at) < 1800)', + name=op.f('ck_orders_on_scheduled_orders_not_within_30_minutes'), + ), + sa.CheckConstraint( + 'NOT (ad_hoc IS FALSE AND ((EXTRACT(HOUR FROM scheduled_delivery_at) <= 11 AND NOT (EXTRACT(HOUR FROM scheduled_delivery_at) = 11 AND EXTRACT(MINUTE FROM scheduled_delivery_at) = 45)) OR EXTRACT(HOUR FROM scheduled_delivery_at) > 22))', # noqa:E501 + name=op.f('ck_orders_on_scheduled_orders_within_business_hours'), + ), + sa.CheckConstraint( + 'NOT (ad_hoc IS TRUE AND (EXTRACT(HOUR FROM placed_at) < 11 OR EXTRACT(HOUR FROM placed_at) > 22))', # noqa:E501 + name=op.f('ck_orders_on_ad_hoc_orders_within_business_hours'), + ), + sa.CheckConstraint( + 'NOT (cancelled IS FALSE AND cancelled_at IS NOT NULL)', + name=op.f('ck_orders_on_only_cancelled_orders_may_have_cancelled_at'), + ), + sa.CheckConstraint( + 'NOT (cancelled IS TRUE AND delivery_at IS NOT NULL)', + name=op.f('ck_orders_on_cancelled_orders_must_not_be_delivered'), + ), + sa.CheckConstraint( + 'cancelled_at > courier_accepted_at', + name=op.f('ck_orders_on_ordered_timestamps_16'), + ), + sa.CheckConstraint( + 'cancelled_at > courier_notified_at', + name=op.f('ck_orders_on_ordered_timestamps_15'), + ), + sa.CheckConstraint( + 'cancelled_at > delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_21'), + ), + sa.CheckConstraint( + 'cancelled_at > dispatch_at', + name=op.f('ck_orders_on_ordered_timestamps_14'), + ), + sa.CheckConstraint( + 'cancelled_at > left_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_19'), + ), + sa.CheckConstraint( + 'cancelled_at > pickup_at', name=op.f('ck_orders_on_ordered_timestamps_18'), + ), + sa.CheckConstraint( + 'cancelled_at > reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_20'), + ), + sa.CheckConstraint( + 'cancelled_at > reached_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_17'), + ), + sa.CheckConstraint( + 'cancelled_at > restaurant_confirmed_at', + name=op.f('ck_orders_on_ordered_timestamps_13'), + ), + sa.CheckConstraint( + 'cancelled_at > restaurant_notified_at', + name=op.f('ck_orders_on_ordered_timestamps_12'), + ), + sa.CheckConstraint( + 'courier_accepted_at < delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_42'), + ), + sa.CheckConstraint( + 'courier_accepted_at < left_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_40'), + ), + sa.CheckConstraint( + 'courier_accepted_at < pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_39'), + ), + sa.CheckConstraint( + 'courier_accepted_at < reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_41'), + ), + sa.CheckConstraint( + 'courier_accepted_at < reached_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_38'), + ), + sa.CheckConstraint( + 'courier_notified_at < courier_accepted_at', + name=op.f('ck_orders_on_ordered_timestamps_32'), + ), + sa.CheckConstraint( + 'courier_notified_at < delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_37'), + ), + sa.CheckConstraint( + 'courier_notified_at < left_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_35'), + ), + sa.CheckConstraint( + 'courier_notified_at < pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_34'), + ), + sa.CheckConstraint( + 'courier_notified_at < reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_36'), + ), + sa.CheckConstraint( + 'courier_notified_at < reached_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_33'), + ), + sa.CheckConstraint( + 'dispatch_at < courier_accepted_at', + name=op.f('ck_orders_on_ordered_timestamps_26'), + ), + sa.CheckConstraint( + 'dispatch_at < courier_notified_at', + name=op.f('ck_orders_on_ordered_timestamps_25'), + ), + sa.CheckConstraint( + 'dispatch_at < delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_31'), + ), + sa.CheckConstraint( + 'dispatch_at < left_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_29'), + ), + sa.CheckConstraint( + 'dispatch_at < pickup_at', name=op.f('ck_orders_on_ordered_timestamps_28'), + ), + sa.CheckConstraint( + 'dispatch_at < reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_30'), + ), + sa.CheckConstraint( + 'dispatch_at < reached_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_27'), + ), + sa.CheckConstraint( + 'estimated_prep_buffer % 60 = 0', + name=op.f('ck_orders_on_estimated_prep_buffer_must_be_whole_minutes'), + ), + sa.CheckConstraint( + 'estimated_prep_duration % 60 = 0', + name=op.f('ck_orders_on_estimated_prep_duration_must_be_whole_minutes'), + ), + sa.CheckConstraint( + 'left_pickup_at < delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_51'), + ), + sa.CheckConstraint( + 'left_pickup_at < reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_50'), + ), + sa.CheckConstraint( + 'pickup_at < delivery_at', name=op.f('ck_orders_on_ordered_timestamps_49'), + ), + sa.CheckConstraint( + 'pickup_at < left_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_47'), + ), + sa.CheckConstraint( + 'pickup_at < reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_48'), + ), + sa.CheckConstraint( + 'placed_at < cancelled_at', name=op.f('ck_orders_on_ordered_timestamps_2'), + ), + sa.CheckConstraint( + 'placed_at < courier_accepted_at', + name=op.f('ck_orders_on_ordered_timestamps_7'), + ), + sa.CheckConstraint( + 'placed_at < courier_notified_at', + name=op.f('ck_orders_on_ordered_timestamps_6'), + ), + sa.CheckConstraint( + 'placed_at < delivery_at', name=op.f('ck_orders_on_ordered_timestamps_11'), + ), + sa.CheckConstraint( + 'placed_at < dispatch_at', name=op.f('ck_orders_on_ordered_timestamps_5'), + ), + sa.CheckConstraint( + 'placed_at < first_estimated_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_1'), + ), + sa.CheckConstraint( + 'placed_at < left_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_9'), + ), + sa.CheckConstraint( + 'placed_at < reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_10'), + ), + sa.CheckConstraint( + 'placed_at < reached_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_8'), + ), + sa.CheckConstraint( + 'placed_at < restaurant_confirmed_at', + name=op.f('ck_orders_on_ordered_timestamps_4'), + ), + sa.CheckConstraint( + 'placed_at < restaurant_notified_at', + name=op.f('ck_orders_on_ordered_timestamps_3'), + ), + sa.CheckConstraint( + 'placed_at < scheduled_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_0'), + ), + sa.CheckConstraint( + 'reached_delivery_at < delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_52'), + ), + sa.CheckConstraint( + 'reached_pickup_at < delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_46'), + ), + sa.CheckConstraint( + 'reached_pickup_at < left_pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_44'), + ), + sa.CheckConstraint( + 'reached_pickup_at < pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_43'), + ), + sa.CheckConstraint( + 'reached_pickup_at < reached_delivery_at', + name=op.f('ck_orders_on_ordered_timestamps_45'), + ), + sa.CheckConstraint( + 'restaurant_confirmed_at < pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_24'), + ), + sa.CheckConstraint( + 'restaurant_notified_at < pickup_at', + name=op.f('ck_orders_on_ordered_timestamps_23'), + ), + sa.CheckConstraint( + 'restaurant_notified_at < restaurant_confirmed_at', + name=op.f('ck_orders_on_ordered_timestamps_22'), + ), + sa.CheckConstraint( + '(pickup_at IS NULL AND pickup_not_confirmed IS NULL) OR (pickup_at IS NOT NULL AND pickup_not_confirmed IS NOT NULL)', # noqa:E501 + name=op.f('pickup_not_confirmed_only_if_pickup'), + ), + sa.CheckConstraint( + '(delivery_at IS NULL AND delivery_not_confirmed IS NULL) OR (delivery_at IS NOT NULL AND delivery_not_confirmed IS NOT NULL)', # noqa:E501 + name=op.f('delivery_not_confirmed_only_if_delivery'), + ), + sa.CheckConstraint( + '(delivery_at IS NULL AND courier_waited_at_delivery IS NULL) OR (delivery_at IS NOT NULL AND courier_waited_at_delivery IS NOT NULL)', # noqa:E501 + name=op.f('courier_waited_at_delivery_only_if_delivery'), + ), + sa.ForeignKeyConstraint( + ['courier_id'], + [f'{config.CLEAN_SCHEMA}.couriers.id'], + name=op.f('fk_orders_to_couriers_via_courier_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['customer_id'], + [f'{config.CLEAN_SCHEMA}.customers.id'], + name=op.f('fk_orders_to_customers_via_customer_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['delivery_address_id'], + [f'{config.CLEAN_SCHEMA}.addresses.id'], + name=op.f('fk_orders_to_addresses_via_delivery_address_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['pickup_address_id'], + [f'{config.CLEAN_SCHEMA}.addresses.id'], + name=op.f('fk_orders_to_addresses_via_pickup_address_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['restaurant_id'], + [f'{config.CLEAN_SCHEMA}.restaurants.id'], + name=op.f('fk_orders_to_restaurants_via_restaurant_id'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.PrimaryKeyConstraint('id', name=op.f('pk_orders')), + *( + [ # noqa:WPS504 + sa.ForeignKeyConstraint( + ['id'], + [f'{config.ORIGINAL_SCHEMA}.orders.id'], + name=op.f('pk_orders_sanity'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + sa.ForeignKeyConstraint( + ['delivery_id'], + [f'{config.ORIGINAL_SCHEMA}.deliveries.id'], + name=op.f('pk_deliveries_sanity'), + onupdate='RESTRICT', + ondelete='RESTRICT', + ), + ] + if not config.TESTING + else [] + ), + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_cancelled'), + 'orders', + ['cancelled'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_cancelled_at_corrected'), + 'orders', + ['cancelled_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_courier_accepted_at_corrected'), + 'orders', + ['courier_accepted_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_courier_id'), + 'orders', + ['courier_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_courier_notified_at_corrected'), + 'orders', + ['courier_notified_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_customer_id'), + 'orders', + ['customer_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_delivery_address_id'), + 'orders', + ['delivery_address_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_delivery_at_corrected'), + 'orders', + ['delivery_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_delivery_id'), + 'orders', + ['delivery_id'], + unique=True, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_dispatch_at_corrected'), + 'orders', + ['dispatch_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_estimated_prep_buffer'), + 'orders', + ['estimated_prep_buffer'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_estimated_prep_duration'), + 'orders', + ['estimated_prep_duration'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_estimated_prep_duration_corrected'), + 'orders', + ['estimated_prep_duration_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_left_pickup_at_corrected'), + 'orders', + ['left_pickup_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_pickup_address_id'), + 'orders', + ['pickup_address_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_pickup_at_corrected'), + 'orders', + ['pickup_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_placed_at'), + 'orders', + ['placed_at'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_restaurant_confirmed_at_corrected'), + 'orders', + ['restaurant_confirmed_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_restaurant_id'), + 'orders', + ['restaurant_id'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_restaurant_notified_at_corrected'), + 'orders', + ['restaurant_notified_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_scheduled_delivery_at'), + 'orders', + ['scheduled_delivery_at'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + op.create_index( + op.f('ix_orders_on_scheduled_delivery_at_corrected'), + 'orders', + ['scheduled_delivery_at_corrected'], + unique=False, + schema=config.CLEAN_SCHEMA, + ) + + +def downgrade(): + """Downgrade to revision None.""" + op.execute(f'DROP SCHEMA {config.CLEAN_SCHEMA} CASCADE;') diff --git a/noxfile.py b/noxfile.py index b8fd263..bf97ca1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -74,7 +74,9 @@ PYTEST_LOCATION = 'tests/' # Paths with all *.py files. SRC_LOCATIONS = ( - f'{DOCS_SRC}/conf.py', + f'{DOCS_SRC}conf.py', + 'migrations/env.py', + 'migrations/versions/', 'noxfile.py', PACKAGE_SOURCE_LOCATION, PYTEST_LOCATION, @@ -235,7 +237,12 @@ def test(session): # non-develop dependencies be installed in the virtual environment. session.run('poetry', 'install', '--no-dev', external=True) _install_packages( - session, 'packaging', 'pytest', 'pytest-cov', 'xdoctest[optional]', + session, + 'packaging', + 'pytest', + 'pytest-cov', + 'pytest-env', + 'xdoctest[optional]', ) # Interpret extra arguments as options for pytest. diff --git a/poetry.lock b/poetry.lock index cac0b0a..54abad8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,20 @@ optional = false python-versions = "*" version = "0.7.12" +[[package]] +category = "main" +description = "A database migration tool for SQLAlchemy." +name = "alembic" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.4.2" + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.1.0" +python-dateutil = "*" +python-editor = ">=0.3" + [[package]] category = "dev" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." @@ -562,7 +576,22 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.4.3" [[package]] -category = "dev" +category = "main" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +name = "mako" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.3" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +category = "main" description = "Safely add untrusted strings to HTML/XML markup." name = "markupsafe" optional = false @@ -812,6 +841,28 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] +[[package]] +category = "dev" +description = "py.test plugin that allows you to add environment variables." +name = "pytest-env" +optional = false +python-versions = "*" +version = "0.6.2" + +[package.dependencies] +pytest = ">=2.6.0" + +[[package]] +category = "main" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" + +[package.dependencies] +six = ">=1.5" + [[package]] category = "main" description = "Add .env support to your django/flask apps in development and deployments" @@ -823,6 +874,14 @@ version = "0.14.0" [package.extras] cli = ["click (>=5.0)"] +[[package]] +category = "main" +description = "Programmatically open an editor, capture the result." +name = "python-editor" +optional = false +python-versions = "*" +version = "1.0.4" + [[package]] category = "dev" description = "World timezone definitions, modern and historical" @@ -877,7 +936,7 @@ version = "1.3.1" docutils = ">=0.11,<1.0" [[package]] -category = "dev" +category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false @@ -1179,7 +1238,7 @@ optional = ["pygments", "colorama"] tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11"] [metadata] -content-hash = "508cbaa3105e47cac64c68663ed8d4178ee752bf267cb24cf68264e73325e10b" +content-hash = "3227fd9a5706b1483adc9b6cb7350515ffda05c38ab9c9a83d63594b3f4f6673" lock-version = "1.0" python-versions = "^3.8" @@ -1188,6 +1247,9 @@ alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] +alembic = [ + {file = "alembic-1.4.2.tar.gz", hash = "sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -1435,6 +1497,10 @@ lazy-object-proxy = [ {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"}, ] +mako = [ + {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, + {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, +] 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"}, @@ -1580,10 +1646,24 @@ pytest-cov = [ {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, ] +pytest-env = [ + {file = "pytest-env-0.6.2.tar.gz", hash = "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"}, +] +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-dotenv = [ {file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"}, {file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"}, ] +python-editor = [ + {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, + {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, + {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"}, + {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, + {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"}, +] pytz = [ {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, diff --git a/pyproject.toml b/pyproject.toml index 4948d98..505526e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ repository = "https://github.com/webartifex/urban-meal-delivery" [tool.poetry.dependencies] python = "^3.8" +alembic = "^1.4.2" click = "^7.1.2" psycopg2 = "^2.8.5" # adapter for PostgreSQL python-dotenv = "^0.14.0" @@ -56,6 +57,7 @@ wemake-python-styleguide = "^0.14.1" # flake8 plug-in packaging = "^20.4" # used to test the packaged version pytest = "^6.0.1" pytest-cov = "^2.10.0" +pytest-env = "^0.6.2" xdoctest = { version="^0.13.0", extras=["optional"] } # Documentation diff --git a/setup.cfg b/setup.cfg index 612d12e..1bbb117 100644 --- a/setup.cfg +++ b/setup.cfg @@ -102,6 +102,20 @@ per-file-ignores = docs/conf.py: # Allow shadowing built-ins and reading __*__ variables. WPS125,WPS609, + migrations/env.py: + # Type annotations are not strictly enforced. + ANN0, ANN2, + migrations/versions/*.py: + # Type annotations are not strictly enforced. + ANN0, ANN2, + # File names of revisions are ok. + WPS114,WPS118, + # Revisions may have too many expressions. + WPS204,WPS213, + # No overuse of string constants (e.g., 'RESTRICT'). + WPS226, + # Too many noqa's are ok. + WPS402, noxfile.py: # Type annotations are not strictly enforced. ANN0, ANN2, @@ -111,7 +125,7 @@ per-file-ignores = WPS213, # No overuse of string constants (e.g., '--version'). WPS226, - src/urban_meal_delivery/_config.py: + src/urban_meal_delivery/configuration.py: # Allow upper case class variables within classes. WPS115, # Numbers are normal in config files. @@ -255,5 +269,7 @@ addopts = --strict-markers cache_dir = .cache/pytest console_output_style = count +env = + TESTING=true markers = e2e: integration tests, inlc., for example, tests touching a database diff --git a/src/urban_meal_delivery/__init__.py b/src/urban_meal_delivery/__init__.py index 676a458..943ba9b 100644 --- a/src/urban_meal_delivery/__init__.py +++ b/src/urban_meal_delivery/__init__.py @@ -9,7 +9,7 @@ Example: import os as _os from importlib import metadata as _metadata -from urban_meal_delivery import _config # noqa:WPS450 +from urban_meal_delivery import configuration as _configuration try: @@ -26,8 +26,8 @@ else: __version__ = _pkg_info['version'] -# Little Hack: "Overwrites" the config module so that the environment is already set. -config: _config.Config = _config.get_config( +# Global `config` object to be used in the package. +config: _configuration.Config = _configuration.make_config( 'testing' if _os.getenv('TESTING') else 'production', ) diff --git a/src/urban_meal_delivery/_config.py b/src/urban_meal_delivery/configuration.py similarity index 80% rename from src/urban_meal_delivery/_config.py rename to src/urban_meal_delivery/configuration.py index 482b95d..0e6eefa 100644 --- a/src/urban_meal_delivery/_config.py +++ b/src/urban_meal_delivery/configuration.py @@ -1,11 +1,12 @@ """Provide package-wide configuration. -This module is "protected" so that it is only used -via the `config` proxy at the package's top level. +This module provides utils to create new `Config` objects +on the fly, mainly for testing and migrating! -That already loads the correct configuration -depending on the current environment. +Within this package, use the `config` proxy at the package's top level +to access the current configuration! """ + import datetime import os import random @@ -20,8 +21,10 @@ dotenv.load_dotenv() def random_schema_name() -> str: """Generate a random PostgreSQL schema name for testing.""" - return ''.join( - random.choice(string.ascii_lowercase) for _ in range(10) # noqa:S311 + return 'temp_{name}'.format( + name=''.join( + (random.choice(string.ascii_lowercase) for _ in range(10)), # noqa:S311 + ), ) @@ -44,6 +47,9 @@ class Config: # The PostgreSQL schema that holds the tables with the cleaned data. CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA') or 'clean' + ALEMBIC_TABLE = 'alembic_version' + ALEMBIC_TABLE_SCHEMA = 'public' + def __repr__(self) -> str: """Non-literal text representation.""" return '' @@ -68,8 +74,8 @@ class TestingConfig(Config): CLEAN_SCHEMA = os.getenv('CLEAN_SCHEMA_TESTING') or random_schema_name() -def get_config(env: str = 'production') -> Config: - """Get the configuration for the package. +def make_config(env: str = 'production') -> Config: + """Create a new `Config` object. Args: env: either 'production' or 'testing'; defaults to the first diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1b91688 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +"""Utils for testing the entire package.""" + +import os + +from urban_meal_delivery import config + + +if not os.getenv('TESTING'): + raise RuntimeError('Tests must be executed with TESTING set in the environment') + +if not config.TESTING: + raise RuntimeError('The testing configuration was not loaded') diff --git a/tests/db/conftest.py b/tests/db/conftest.py index eeca169..2508161 100644 --- a/tests/db/conftest.py +++ b/tests/db/conftest.py @@ -3,30 +3,49 @@ import datetime import pytest -from sqlalchemy import schema +from alembic import command as migrations_cmd +from alembic import config as migrations_config from urban_meal_delivery import config from urban_meal_delivery import db -@pytest.fixture(scope='session') -def db_engine(): +@pytest.fixture(scope='session', params=['all_at_once', 'sequentially']) +def db_engine(request): """Create all tables given the ORM models. The tables are put into a distinct PostgreSQL schema that is removed after all tests are over. The engine used to do that is yielded. + + There are two modes for this fixture: + + - "all_at_once": build up the tables all at once with MetaData.create_all() + - "sequentially": build up the tables sequentially with `alembic upgrade head` + + This ensures that Alembic's migration files are consistent. """ engine = db.make_engine() - engine.execute(schema.CreateSchema(config.CLEAN_SCHEMA)) - db.Base.metadata.create_all(engine) + + if request.param == 'all_at_once': + engine.execute(f'CREATE SCHEMA {config.CLEAN_SCHEMA};') + db.Base.metadata.create_all(engine) + else: + cfg = migrations_config.Config('alembic.ini') + migrations_cmd.upgrade(cfg, 'head') try: yield engine finally: - engine.execute(schema.DropSchema(config.CLEAN_SCHEMA, cascade=True)) + engine.execute(f'DROP SCHEMA {config.CLEAN_SCHEMA} CASCADE;') + + if request.param == 'sequentially': + tmp_alembic_version = f'{config.ALEMBIC_TABLE}_{config.CLEAN_SCHEMA}' + engine.execute( + f'DROP TABLE {config.ALEMBIC_TABLE_SCHEMA}.{tmp_alembic_version};', + ) @pytest.fixture diff --git a/tests/test_config.py b/tests/test_config.py index 04c79f5..6569161 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,7 @@ import pytest -from urban_meal_delivery import _config as config_mod # noqa:WPS450 +from urban_meal_delivery import configuration envs = ['production', 'testing'] @@ -11,7 +11,7 @@ envs = ['production', 'testing'] @pytest.mark.parametrize('env', envs) def test_config_repr(env): """Config objects have the text representation ''.""" - config = config_mod.get_config(env) + config = configuration.make_config(env) assert str(config) == '' @@ -19,18 +19,18 @@ def test_config_repr(env): def test_invalid_config(): """There are only 'production' and 'testing' configurations.""" with pytest.raises(ValueError, match="'production' or 'testing'"): - config_mod.get_config('invalid') + configuration.make_config('invalid') @pytest.mark.parametrize('env', envs) def test_database_uri_set(env, monkeypatch): """Package does NOT emit warning if DATABASE_URI is set.""" uri = 'postgresql://user:password@localhost/db' - monkeypatch.setattr(config_mod.ProductionConfig, 'DATABASE_URI', uri) - monkeypatch.setattr(config_mod.TestingConfig, 'DATABASE_URI', uri) + monkeypatch.setattr(configuration.ProductionConfig, 'DATABASE_URI', uri) + monkeypatch.setattr(configuration.TestingConfig, 'DATABASE_URI', uri) with pytest.warns(None) as record: - config_mod.get_config(env) + configuration.make_config(env) assert len(record) == 0 # noqa:WPS441,WPS507 @@ -38,16 +38,17 @@ def test_database_uri_set(env, monkeypatch): @pytest.mark.parametrize('env', envs) def test_no_database_uri_set(env, monkeypatch): """Package does not work without DATABASE_URI set in the environment.""" - monkeypatch.setattr(config_mod.ProductionConfig, 'DATABASE_URI', None) - monkeypatch.setattr(config_mod.TestingConfig, 'DATABASE_URI', None) + monkeypatch.setattr(configuration.ProductionConfig, 'DATABASE_URI', None) + monkeypatch.setattr(configuration.TestingConfig, 'DATABASE_URI', None) with pytest.warns(UserWarning, match='no DATABASE_URI'): - config_mod.get_config(env) + configuration.make_config(env) def test_random_testing_schema(): - """CLEAN_SCHEMA is randomized if not seti explicitly.""" - result = config_mod.random_schema_name() + """CLEAN_SCHEMA is randomized if not set explicitly.""" + result = configuration.random_schema_name() assert isinstance(result, str) - assert len(result) <= 10 + assert result.startswith('temp_') + assert len(result) == 15 From 49ba0c433e3c90833d7f1b692d3d38b184d525f5 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Sun, 9 Aug 2020 20:21:34 +0200 Subject: [PATCH 06/14] Fix the "clean-pwd" command in nox - some glob patterns in .gitignore were not correctly expanded - adapt the exclude logic to focus on the start of the excluded paths --- .gitignore | 3 ++- noxfile.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 84d0bf6..2fc8820 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .cache/ -*.egg-info/ +**/*.egg-info/ .env +**/.ipynb_checkpoints/ .python-version .venv/ diff --git a/noxfile.py b/noxfile.py index bf97ca1..ec5d6f9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -357,25 +357,29 @@ def init_project(session): @nox.session(name='clean-pwd', python=PYTHON, venv_backend='none') -def clean_pwd(session): +def clean_pwd(session): # noqa:WPS210,WPS231 """Remove (almost) all glob patterns listed in .gitignore. The difference compared to `git clean -X` is that this task does not remove pyenv's .python-version file and poetry's virtual environment. """ - exclude = frozenset(('.python-version', '.venv', 'venv')) + exclude = frozenset(('.env', '.python-version', '.venv/', 'venv/')) with open('.gitignore') as file_handle: paths = file_handle.readlines() for path in paths: path = path.strip() - if path.startswith('#') or path in exclude: + if path.startswith('#'): continue for expanded in glob.glob(path): - session.run(f'rm -rf {expanded}') + for excluded in exclude: + if expanded.startswith(excluded): + break + else: + session.run('rm', '-rf', expanded) def _begin(session): From ac5804174d8411a2deb7183f48f2081ee2801459 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Mon, 10 Aug 2020 16:51:02 +0200 Subject: [PATCH 07/14] Add a branch reference fixer as a pre-commit hook - many *.py and *.ipynb files will contain links to resources on GitHub or nbviewer that have branch references in them - add a pre-commit hook implemented as the nox session "fix-branch-references" that goes through these files and changes all the branch labels to the current one --- .pre-commit-config.yaml | 6 +++ noxfile.py | 107 +++++++++++++++++++++++++++++++++++++--- setup.cfg | 4 +- 3 files changed, 108 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cefbf7e..0e1f8f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,12 @@ repos: language: system stages: [commit] types: [python] + - id: local-fix-branch-references + name: Adjust the branch references + entry: poetry run nox -s fix-branch-references -- + language: system + stages: [commit] + types: [text] - id: local-pre-merge-checks name: Run the entire test suite entry: poetry run nox -s pre-merge -- diff --git a/noxfile.py b/noxfile.py index ec5d6f9..d12a3f5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -54,12 +54,17 @@ The pre-commit framework invokes the "pre-commit" and "pre-merge" sessions: import contextlib import glob import os +import re +import shutil +import subprocess # noqa:S404 import tempfile +from typing import Generator, IO, Tuple import nox from nox.sessions import Session +GITHUB_REPOSITORY = 'webartifex/urban-meal-delivery' PACKAGE_IMPORT_NAME = 'urban_meal_delivery' # Docs/sphinx locations. @@ -349,6 +354,94 @@ def pre_merge(session): test(session) +@nox.session(name='fix-branch-references', python=PYTHON, venv_backend='none') +def fix_branch_references(_): # noqa:WPS210 + """Replace branch references with the current branch. + + Intended to be run as a pre-commit hook. + + Many files in the project (e.g., README.md) contain links to resources + on github.com or nbviewer.jupyter.org that contain branch labels. + + This task rewrites these links such that they contain the branch reference + of the current branch. + """ + # Adjust this to add/remove glob patterns + # whose links are re-written. + paths = ['*.md', '**/*.md', '**/*.ipynb'] + + branch = ( + subprocess.check_output( # noqa:S603 + ('git', 'rev-parse', '--abbrev-ref', 'HEAD'), + ) + .decode() + .strip() + ) + + rewrites = [ + { + 'name': 'github', + 'pattern': re.compile( + fr'((((http)|(https))://github\.com/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w-]+)/)', # noqa:E501 + ), + 'replacement': fr'\2{branch}/', + }, + { + 'name': 'nbviewer', + 'pattern': re.compile( + fr'((((http)|(https))://nbviewer\.jupyter\.org/github/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w-]+)/)', # noqa:E501 + ), + 'replacement': fr'\2{branch}/', + }, + ] + + for expanded in _expand(*paths): + with _line_by_line_replace(expanded) as (old_file, new_file): + for line in old_file: + for rewrite in rewrites: + line = re.sub(rewrite['pattern'], rewrite['replacement'], line) + new_file.write(line) + + +def _expand(*patterns: str) -> Generator[str, None, None]: + """Expand glob patterns into paths. + + Args: + *patterns: the patterns to be expanded + + Yields: + expanded: a single expanded path + """ # noqa:RST213 + for pattern in patterns: + yield from glob.glob(pattern.strip()) + + +@contextlib.contextmanager +def _line_by_line_replace(path: str) -> Generator[Tuple[IO, IO], None, None]: + """Replace/change the lines in a file one by one. + + This generator function yields two file handles, one to the current file + (i.e., `old_file`) and one to its replacement (i.e., `new_file`). + + Usage: loop over the lines in `old_file` and write the files to be kept + to `new_file`. Files not written to `new_file` are removed! + + Args: + path: the file whose lines are to be replaced + + Yields: + old_file, new_file: handles to a file and its replacement + """ + file_handle, new_file_path = tempfile.mkstemp() + with os.fdopen(file_handle, 'w') as new_file: + with open(path) as old_file: + yield old_file, new_file + + shutil.copymode(path, new_file_path) + os.remove(path) + shutil.move(new_file_path, path) + + @nox.session(name='init-project', python=PYTHON, venv_backend='none') def init_project(session): """Install the pre-commit hooks.""" @@ -369,17 +462,15 @@ def clean_pwd(session): # noqa:WPS210,WPS231 with open('.gitignore') as file_handle: paths = file_handle.readlines() - for path in paths: - path = path.strip() + for path in _expand(*paths): if path.startswith('#'): continue - for expanded in glob.glob(path): - for excluded in exclude: - if expanded.startswith(excluded): - break - else: - session.run('rm', '-rf', expanded) + for excluded in exclude: + if path.startswith(excluded): + break + else: + session.run('rm', '-rf', path) def _begin(session): diff --git a/setup.cfg b/setup.cfg index 1bbb117..5bbd00d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,7 +88,7 @@ extend-ignore = B950, # Comply with black's style. # Source: https://github.com/psf/black/blob/master/docs/compatible_configs.md#flake8 - E203, W503, + E203, W503, WPS348, # f-strings are ok. WPS305, # Classes should not have to specify a base class. @@ -125,6 +125,8 @@ per-file-ignores = WPS213, # No overuse of string constants (e.g., '--version'). WPS226, + # The noxfile is rather long => allow many noqa's. + WPS402, src/urban_meal_delivery/configuration.py: # Allow upper case class variables within classes. WPS115, From 4ee5a50fc695fe64df910c7ad619dfe9887d9c69 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Mon, 10 Aug 2020 16:55:35 +0200 Subject: [PATCH 08/14] Simplify the pre-commit hooks - run "format" and "lint" separately => remove the nox session "pre-commit" - execute the entire "test-suite" before merges + rename "pre-merge" into "test-suite" + do not "format" and "lint" here any more + do not execute this before pushes * allow branches with <100% test coverage to exist on GitHub (they cannot be merged into 'main' until 100% coverage) * GitHub Actions executes the test suite --- .pre-commit-config.yaml | 20 ++++++++++------ noxfile.py | 51 ++++++++++------------------------------- 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e1f8f1..987b01d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,9 +4,15 @@ repos: # Run the local formatting, linting, and testing tool chains. - repo: local hooks: - - id: local-pre-commit-checks - name: Run code formatters and linters - entry: poetry run nox -s pre-commit -- + - id: local-format + name: Format the source files + entry: poetry run nox -s format -- + language: system + stages: [commit] + types: [python] + - id: local-lint + name: Lint the source files + entry: poetry run nox -s lint -- language: system stages: [commit] types: [python] @@ -16,12 +22,12 @@ repos: language: system stages: [commit] types: [text] - - id: local-pre-merge-checks + - id: local-test-suite name: Run the entire test suite - entry: poetry run nox -s pre-merge -- + entry: poetry run nox -s test-suite -- language: system - stages: [merge-commit, push] - types: [python] + stages: [merge-commit] + types: [text] # Enable hooks provided by the pre-commit project to # enforce rules that local tools could not that easily. - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/noxfile.py b/noxfile.py index d12a3f5..49e49e0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -27,7 +27,7 @@ as unified tasks to assure the quality of the source code: => may be paths or options -GitHub Actions implements a CI workflow: +GitHub Actions implements the following CI workflow: - "format", "lint", and "test" as above @@ -36,18 +36,14 @@ GitHub Actions implements a CI workflow: - "docs": build the documentation with sphinx -The pre-commit framework invokes the "pre-commit" and "pre-merge" sessions: +The pre-commit framework invokes the following tasks: -- "pre-commit" before all commits: +- before any commit: - + triggers "format" and "lint" on staged source files - + => test coverage may be < 100% + + "format" and "lint" as above + + "fix-branch-references": replace branch references with the current one -- "pre-merge" before all merges and pushes: - - + same as "pre-commit" - + plus: triggers "test", "safety", and "docs" (that ignore extra arguments) - + => test coverage is enforced to be 100% +- before merges: run the entire "test-suite" independent of the file changes """ @@ -199,23 +195,6 @@ def lint(session): ) -@nox.session(name='pre-commit', python=PYTHON, venv_backend='none') -def pre_commit(session): - """Run the format and lint sessions. - - Source files must be well-formed before they enter git. - - Intended to be run as a pre-commit hook. - - Passed in extra arguments are forwarded. So, if it is run as a pre-commit - hook, only the currently staged source files are formatted and linted. - """ - # "format" and "lint" are run in sessions on their own as - # session.notify() creates new Session objects. - session.notify('format') - session.notify('lint') - - @nox.session(python=PYTHON) def test(session): """Test the code base. @@ -320,28 +299,22 @@ def docs(session): print(f'Docs are available at {os.getcwd()}/{DOCS_BUILD}index.html') # noqa:WPS421 -@nox.session(name='pre-merge', python=PYTHON) -def pre_merge(session): - """Run the format, lint, test, safety, and docs sessions. +@nox.session(name='test-suite', python=PYTHON) +def test_suite(session): + """Run the entire test suite. - Intended to be run either as a pre-merge or pre-push hook. + Intended to be run as a pre-commit hook. Ignores the paths passed in by the pre-commit framework - for the test, safety, and docs sessions so that the - entire test suite is executed. + and runs the entire test suite. """ # Re-using an old environment is not so easy here as the "test" session # runs `poetry install --no-dev`, which removes previously installed packages. if session.virtualenv.reuse_existing: raise RuntimeError( - 'The "pre-merge" session must be run without the "-r" option', + 'The "test-suite" session must be run without the "-r" option', ) - session.notify('format') - session.notify('lint') - session.notify('safety') - session.notify('docs') - # Little hack to not work with the extra arguments provided # by the pre-commit framework. Create a flag in the # env(ironment) that must contain only `str`-like objects. From ebf16b50d9e6034c71e369eb7d46de6b774355c1 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 11 Aug 2020 10:50:29 +0200 Subject: [PATCH 09/14] Add Jupyter Lab environment - dependencies used to run the Jupyter Lab environment that are not required by the `urban-meal-delivery` package itself are put into an installation extra called "research" - this allows to NOT install the requirements, for example, when testing the package in an isolated environment --- poetry.lock | 884 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 18 + 2 files changed, 889 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index 54abad8..4f5dafa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -28,6 +28,15 @@ optional = false python-versions = "*" version = "1.4.4" +[[package]] +category = "main" +description = "Disable App Nap on OS X 10.9" +marker = "sys_platform == \"darwin\" or platform_system == \"Darwin\"" +name = "appnope" +optional = true +python-versions = "*" +version = "0.1.0" + [[package]] category = "dev" description = "Bash tab completion for argparse" @@ -39,6 +48,23 @@ version = "1.12.0" [package.extras] test = ["coverage", "flake8", "pexpect", "wheel"] +[[package]] +category = "main" +description = "The secure Argon2 password hashing algorithm." +name = "argon2-cffi" +optional = true +python-versions = "*" +version = "20.1.0" + +[package.dependencies] +cffi = ">=1.0.0" +six = "*" + +[package.extras] +dev = ["coverage (>=5.0.2)", "hypothesis", "pytest", "sphinx", "wheel", "pre-commit"] +docs = ["sphinx"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pytest"] + [[package]] category = "dev" description = "Read/rewrite/write Python ASTs" @@ -81,7 +107,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.4.0" [[package]] -category = "dev" +category = "main" description = "Classes Without Boilerplate" name = "attrs" optional = false @@ -116,6 +142,14 @@ version = "2.8.0" [package.dependencies] pytz = ">=2015.7" +[[package]] +category = "main" +description = "Specifications for callback functions passed in to an API" +name = "backcall" +optional = true +python-versions = "*" +version = "0.2.0" + [[package]] category = "dev" description = "Security oriented static analyser for python code." @@ -152,13 +186,37 @@ typed-ast = ">=1.4.0" d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] -category = "dev" +category = "main" +description = "An easy safelist-based HTML-sanitizing tool." +name = "bleach" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "3.1.5" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + +[[package]] +category = "main" description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" version = "2020.6.20" +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = true +python-versions = "*" +version = "1.14.1" + +[package.dependencies] +pycparser = "*" + [[package]] category = "dev" description = "Validate configuration and produce human readable error messages." @@ -168,7 +226,7 @@ python-versions = ">=3.6.1" version = "3.2.0" [[package]] -category = "dev" +category = "main" description = "Universal encoding detector for Python 2 and 3" name = "chardet" optional = false @@ -184,7 +242,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "7.1.2" [[package]] -category = "dev" +category = "main" description = "Cross-platform colored terminal text." marker = "sys_platform == \"win32\" or platform_system == \"Windows\" or platform_system == \"Windows\"" name = "colorama" @@ -222,6 +280,22 @@ optional = false python-versions = ">=3.5,<4.0" version = "1.5.2" +[[package]] +category = "main" +description = "Decorators for Humans" +name = "decorator" +optional = true +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.4.2" + +[[package]] +category = "main" +description = "XML bomb protection for Python stdlib modules" +name = "defusedxml" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.6.0" + [[package]] category = "dev" description = "Distribution utilities" @@ -238,6 +312,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.16" +[[package]] +category = "main" +description = "Discover and load entry points from installed packages." +name = "entrypoints" +optional = true +python-versions = ">=2.7" +version = "0.3" + [[package]] category = "dev" description = "Removes commented-out code." @@ -516,7 +598,7 @@ version = "1.4.25" license = ["editdistance"] [[package]] -category = "dev" +category = "main" description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false @@ -539,6 +621,64 @@ optional = false python-versions = "*" version = "1.0.1" +[[package]] +category = "main" +description = "IPython Kernel for Jupyter" +name = "ipykernel" +optional = true +python-versions = ">=3.5" +version = "5.3.4" + +[package.dependencies] +appnope = "*" +ipython = ">=5.0.0" +jupyter-client = "*" +tornado = ">=4.2" +traitlets = ">=4.1.0" + +[package.extras] +test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose"] + +[[package]] +category = "main" +description = "IPython: Productive Interactive Computing" +name = "ipython" +optional = true +python-versions = ">=3.7" +version = "7.17.0" + +[package.dependencies] +appnope = "*" +backcall = "*" +colorama = "*" +decorator = "*" +jedi = ">=0.10" +pexpect = "*" +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +setuptools = ">=18.5" +traitlets = ">=4.2" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "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.14)"] + +[[package]] +category = "main" +description = "Vestigial utilities from IPython" +name = "ipython-genutils" +optional = true +python-versions = "*" +version = "0.2.0" + [[package]] category = "dev" description = "A Python utility / library to sort Python imports." @@ -554,7 +694,22 @@ requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs (>=1.4.0)"] [[package]] -category = "dev" +category = "main" +description = "An autocompletion tool for Python that can be used for text editors." +name = "jedi" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.17.2" + +[package.dependencies] +parso = ">=0.7.0,<0.8.0" + +[package.extras] +qa = ["flake8 (3.7.9)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] + +[[package]] +category = "main" description = "A very fast and expressive template engine." name = "jinja2" optional = false @@ -567,6 +722,101 @@ MarkupSafe = ">=0.23" [package.extras] i18n = ["Babel (>=0.8)"] +[[package]] +category = "main" +description = "A Python implementation of the JSON5 data format." +name = "json5" +optional = true +python-versions = "*" +version = "0.9.5" + +[package.extras] +dev = ["hypothesis"] + +[[package]] +category = "main" +description = "An implementation of JSON Schema validation for Python" +name = "jsonschema" +optional = true +python-versions = "*" +version = "3.2.0" + +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0" +setuptools = "*" +six = ">=1.11.0" + +[package.extras] +format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] +format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] + +[[package]] +category = "main" +description = "Jupyter protocol implementation and client libraries" +name = "jupyter-client" +optional = true +python-versions = ">=3.5" +version = "6.1.6" + +[package.dependencies] +jupyter-core = ">=4.6.0" +python-dateutil = ">=2.1" +pyzmq = ">=13" +tornado = ">=4.1" +traitlets = "*" + +[package.extras] +test = ["async-generator", "ipykernel", "ipython", "mock", "pytest", "pytest-asyncio", "pytest-timeout"] + +[[package]] +category = "main" +description = "Jupyter core package. A base package on which Jupyter projects rely." +name = "jupyter-core" +optional = true +python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,!=3.4,>=2.7" +version = "4.6.3" + +[package.dependencies] +pywin32 = ">=1.0" +traitlets = "*" + +[[package]] +category = "main" +description = "The JupyterLab notebook server extension." +name = "jupyterlab" +optional = true +python-versions = ">=3.5" +version = "2.2.4" + +[package.dependencies] +jinja2 = ">=2.10" +jupyterlab-server = ">=1.1.5,<2.0" +notebook = ">=4.3.1" +tornado = "<6.0.0 || >6.0.0,<6.0.1 || >6.0.1,<6.0.2 || >6.0.2" + +[package.extras] +docs = ["jsx-lexer", "recommonmark", "sphinx", "sphinx-rtd-theme", "sphinx-copybutton"] +test = ["pytest", "pytest-check-links", "requests", "wheel", "virtualenv"] + +[[package]] +category = "main" +description = "JupyterLab Server" +name = "jupyterlab-server" +optional = true +python-versions = ">=3.5" +version = "1.2.0" + +[package.dependencies] +jinja2 = ">=2.10" +json5 = "*" +jsonschema = ">=3.0.1" +notebook = ">=4.2.0" +requests = "*" + +[package.extras] +test = ["pytest", "requests"] + [[package]] category = "dev" description = "A fast and thorough lazy object proxy." @@ -606,6 +856,14 @@ optional = false python-versions = "*" version = "0.6.1" +[[package]] +category = "main" +description = "The fastest markdown parser in pure Python" +name = "mistune" +optional = true +python-versions = "*" +version = "0.8.4" + [[package]] category = "dev" description = "More routines for operating on iterables, beyond itertools" @@ -638,6 +896,62 @@ optional = false python-versions = "*" version = "0.4.3" +[[package]] +category = "main" +description = "A simple extension for Jupyter Notebook and Jupyter Lab to beautify Python code automatically using Black." +name = "nb-black" +optional = true +python-versions = "*" +version = "1.0.7" + +[package.dependencies] +ipython = "*" + +[[package]] +category = "main" +description = "Converting Jupyter Notebooks" +name = "nbconvert" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.6.1" + +[package.dependencies] +bleach = "*" +defusedxml = "*" +entrypoints = ">=0.2.2" +jinja2 = ">=2.4" +jupyter-core = "*" +mistune = ">=0.8.1,<2" +nbformat = ">=4.4" +pandocfilters = ">=1.4.1" +pygments = "*" +testpath = "*" +traitlets = ">=4.2" + +[package.extras] +all = ["pytest", "pytest-cov", "ipykernel", "jupyter-client (>=5.3.1)", "ipywidgets (>=7)", "pebble", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "sphinxcontrib-github-alt", "ipython", "mock"] +docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "sphinxcontrib-github-alt", "ipython", "jupyter-client (>=5.3.1)"] +execute = ["jupyter-client (>=5.3.1)"] +serve = ["tornado (>=4.0)"] +test = ["pytest", "pytest-cov", "ipykernel", "jupyter-client (>=5.3.1)", "ipywidgets (>=7)", "pebble", "mock"] + +[[package]] +category = "main" +description = "The Jupyter Notebook format" +name = "nbformat" +optional = true +python-versions = ">=3.5" +version = "5.0.7" + +[package.dependencies] +ipython-genutils = "*" +jsonschema = ">=2.4,<2.5.0 || >2.5.0" +jupyter-core = "*" +traitlets = ">=4.1" + +[package.extras] +test = ["pytest", "pytest-cov", "testpath"] + [[package]] category = "dev" description = "Node.js virtual environment builder" @@ -646,6 +960,34 @@ optional = false python-versions = "*" version = "1.4.0" +[[package]] +category = "main" +description = "A web-based notebook environment for interactive computing" +name = "notebook" +optional = true +python-versions = ">=3.5" +version = "6.1.1" + +[package.dependencies] +Send2Trash = "*" +argon2-cffi = "*" +ipykernel = "*" +ipython-genutils = "*" +jinja2 = "*" +jupyter-client = ">=5.3.4" +jupyter-core = ">=4.6.1" +nbconvert = "*" +nbformat = "*" +prometheus-client = "*" +pyzmq = ">=17" +terminado = ">=0.8.3" +tornado = ">=5.0" +traitlets = ">=4.2.1" + +[package.extras] +docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt"] +test = ["nose", "coverage", "requests", "nose-warnings-filters", "nbval", "nose-exclude", "selenium", "pytest", "pytest-cov", "requests-unixsocket"] + [[package]] category = "dev" description = "Flexible test automation." @@ -664,7 +1006,15 @@ virtualenv = ">=14.0.0" tox_to_nox = ["jinja2", "tox"] [[package]] -category = "dev" +category = "main" +description = "NumPy is the fundamental package for array computing with Python." +name = "numpy" +optional = true +python-versions = ">=3.6" +version = "1.19.1" + +[[package]] +category = "main" description = "Core utilities for Python packages" name = "packaging" optional = false @@ -675,6 +1025,41 @@ version = "20.4" pyparsing = ">=2.0.2" six = "*" +[[package]] +category = "main" +description = "Powerful data structures for data analysis, time series, and statistics" +name = "pandas" +optional = true +python-versions = ">=3.6.1" +version = "1.1.0" + +[package.dependencies] +numpy = ">=1.15.4" +python-dateutil = ">=2.7.3" +pytz = ">=2017.2" + +[package.extras] +test = ["pytest (>=4.0.2)", "pytest-xdist", "hypothesis (>=3.58)"] + +[[package]] +category = "main" +description = "Utilities for writing pandoc filters in python" +name = "pandocfilters" +optional = true +python-versions = "*" +version = "1.4.2" + +[[package]] +category = "main" +description = "A Python Parser" +name = "parso" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.7.1" + +[package.extras] +testing = ["docopt", "pytest (>=3.0.7)"] + [[package]] category = "dev" description = "Utility library for gitignore style pattern matching of file paths." @@ -702,6 +1087,26 @@ version = "0.9.1" [package.dependencies] flake8-polyfill = ">=1.0.2,<2" +[[package]] +category = "main" +description = "Pexpect allows easy control of interactive console applications." +marker = "sys_platform != \"win32\"" +name = "pexpect" +optional = true +python-versions = "*" +version = "4.8.0" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +category = "main" +description = "Tiny 'shelve'-like database with concurrency support" +name = "pickleshare" +optional = true +python-versions = "*" +version = "0.7.5" + [[package]] category = "dev" description = "plugin and hook calling mechanisms for python" @@ -729,6 +1134,28 @@ pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" +[[package]] +category = "main" +description = "Python client for the Prometheus monitoring system." +name = "prometheus-client" +optional = true +python-versions = "*" +version = "0.8.0" + +[package.extras] +twisted = ["twisted"] + +[[package]] +category = "main" +description = "Library for building powerful interactive command lines in Python" +name = "prompt-toolkit" +optional = true +python-versions = ">=3.6.1" +version = "3.0.6" + +[package.dependencies] +wcwidth = "*" + [[package]] category = "main" description = "psycopg2 - Python-PostgreSQL Database Adapter" @@ -737,6 +1164,15 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" version = "2.8.5" +[[package]] +category = "main" +description = "Run a subprocess in a pseudo terminal" +marker = "sys_platform != \"win32\" or os_name != \"nt\"" +name = "ptyprocess" +optional = true +python-versions = "*" +version = "0.6.0" + [[package]] category = "dev" description = "library with cross-python path, ini-parsing, io, code, log facilities" @@ -753,6 +1189,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.6.0" +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.20" + [[package]] category = "dev" description = "Python docstring style checker" @@ -773,7 +1217,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.2.0" [[package]] -category = "dev" +category = "main" description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false @@ -796,13 +1240,24 @@ mccabe = ">=0.6,<0.7" toml = ">=0.7.1" [[package]] -category = "dev" +category = "main" description = "Python parsing module" name = "pyparsing" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" version = "2.4.7" +[[package]] +category = "main" +description = "Persistent/Functional/Immutable data structures" +name = "pyrsistent" +optional = true +python-versions = "*" +version = "0.16.0" + +[package.dependencies] +six = "*" + [[package]] category = "dev" description = "pytest: simple powerful testing with Python" @@ -883,13 +1338,31 @@ python-versions = "*" version = "1.0.4" [[package]] -category = "dev" +category = "main" description = "World timezone definitions, modern and historical" name = "pytz" optional = false python-versions = "*" version = "2020.1" +[[package]] +category = "main" +description = "Python for Window Extensions" +marker = "sys_platform == \"win32\"" +name = "pywin32" +optional = true +python-versions = "*" +version = "228" + +[[package]] +category = "main" +description = "Python bindings for the winpty library" +marker = "os_name == \"nt\"" +name = "pywinpty" +optional = true +python-versions = "*" +version = "0.5.7" + [[package]] category = "dev" description = "YAML parser and emitter for Python" @@ -898,6 +1371,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "5.3.1" +[[package]] +category = "main" +description = "Python bindings for 0MQ" +name = "pyzmq" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" +version = "19.0.2" + [[package]] category = "dev" description = "Alternative regular expression module, to replace re." @@ -907,7 +1388,7 @@ python-versions = "*" version = "2020.7.14" [[package]] -category = "dev" +category = "main" description = "Python HTTP for Humans." name = "requests" optional = false @@ -935,6 +1416,14 @@ version = "1.3.1" [package.dependencies] docutils = ">=0.11,<1.0" +[[package]] +category = "main" +description = "Send file to trash natively under Mac OS X, Windows and Linux." +name = "send2trash" +optional = true +python-versions = "*" +version = "1.5.0" + [[package]] category = "main" description = "Python 2 and 3 compatibility utilities" @@ -1108,6 +1597,19 @@ version = "3.2.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" +[[package]] +category = "main" +description = "Terminals served to xterm.js using Tornado websockets" +name = "terminado" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.8.3" + +[package.dependencies] +ptyprocess = "*" +pywinpty = ">=0.5" +tornado = ">=4" + [[package]] category = "dev" description = "A collection of helpers and mock objects for unit tests and doc tests." @@ -1121,6 +1623,17 @@ build = ["setuptools-git", "wheel", "twine"] docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +[[package]] +category = "main" +description = "Test utilities for code working with files and commands" +name = "testpath" +optional = true +python-versions = "*" +version = "0.4.4" + +[package.extras] +test = ["pathlib2"] + [[package]] category = "dev" description = "Python Library for Tom's Obvious, Minimal Language" @@ -1129,6 +1642,30 @@ optional = false python-versions = "*" version = "0.10.1" +[[package]] +category = "main" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +name = "tornado" +optional = true +python-versions = ">= 3.5" +version = "6.0.4" + +[[package]] +category = "main" +description = "Traitlets Python config system" +name = "traitlets" +optional = true +python-versions = "*" +version = "4.3.3" + +[package.dependencies] +decorator = "*" +ipython-genutils = "*" +six = "*" + +[package.extras] +test = ["pytest", "mock"] + [[package]] category = "dev" description = "a fork of Python 2 and 3 ast modules with type comment support" @@ -1146,7 +1683,7 @@ python-versions = "*" version = "3.7.4.2" [[package]] -category = "dev" +category = "main" description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" optional = false @@ -1176,6 +1713,22 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +[[package]] +category = "main" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = true +python-versions = "*" +version = "0.2.5" + +[[package]] +category = "main" +description = "Character encoding aliases for legacy web content" +name = "webencodings" +optional = true +python-versions = "*" +version = "0.5.1" + [[package]] category = "dev" description = "The strictest and most opinionated python linter ever" @@ -1237,8 +1790,11 @@ all = ["six", "pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja optional = ["pygments", "colorama"] tests = ["pytest", "pytest-cov", "codecov", "scikit-build", "cmake", "ninja", "pybind11"] +[extras] +research = ["jupyterlab", "nb_black", "numpy", "pandas", "pytz"] + [metadata] -content-hash = "3227fd9a5706b1483adc9b6cb7350515ffda05c38ab9c9a83d63594b3f4f6673" +content-hash = "eba980d4335eef2012a1e7ce27941731149eb224cdfad856aa0bcd7701e9e557" lock-version = "1.0" python-versions = "^3.8" @@ -1254,10 +1810,32 @@ 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.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, + {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, +] argcomplete = [ {file = "argcomplete-1.12.0-py2.py3-none-any.whl", hash = "sha256:91dc7f9c7f6281d5a0dce5e73d2e33283aaef083495c13974a7dd197a1cdc949"}, {file = "argcomplete-1.12.0.tar.gz", hash = "sha256:2fbe5ed09fd2c1d727d4199feca96569a5b50d44c71b16da9c742201f7cc295c"}, ] +argon2-cffi = [ + {file = "argon2-cffi-20.1.0.tar.gz", hash = "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-win32.whl", hash = "sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc"}, + {file = "argon2_cffi-20.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe"}, + {file = "argon2_cffi-20.1.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647"}, + {file = "argon2_cffi-20.1.0-cp35-cp35m-win32.whl", hash = "sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361"}, + {file = "argon2_cffi-20.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b"}, + {file = "argon2_cffi-20.1.0-cp36-cp36m-win32.whl", hash = "sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496"}, + {file = "argon2_cffi-20.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa"}, + {file = "argon2_cffi-20.1.0-cp37-abi3-macosx_10_6_intel.whl", hash = "sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b"}, + {file = "argon2_cffi-20.1.0-cp37-cp37m-win32.whl", hash = "sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5"}, + {file = "argon2_cffi-20.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203"}, + {file = "argon2_cffi-20.1.0-cp38-cp38-win32.whl", hash = "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78"}, + {file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"}, +] astor = [ {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, @@ -1285,6 +1863,10 @@ babel = [ {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, ] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] bandit = [ {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, @@ -1293,10 +1875,44 @@ black = [ {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, ] +bleach = [ + {file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"}, + {file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"}, +] certifi = [ {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, ] +cffi = [ + {file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"}, + {file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"}, + {file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"}, + {file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"}, + {file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"}, + {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"}, + {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"}, + {file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"}, + {file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"}, + {file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"}, + {file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"}, + {file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"}, + {file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"}, + {file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"}, + {file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"}, + {file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"}, + {file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"}, + {file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"}, + {file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"}, + {file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"}, + {file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"}, + {file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"}, + {file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"}, + {file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"}, + {file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"}, + {file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"}, + {file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"}, + {file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"}, +] cfgv = [ {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, @@ -1357,6 +1973,14 @@ darglint = [ {file = "darglint-1.5.2-py3-none-any.whl", hash = "sha256:049a98cf3aec8cf6ea344a863c68112d80b7f8de214459b5fa6853371f89c3e7"}, {file = "darglint-1.5.2.tar.gz", hash = "sha256:6b9461f96694c2cf1d8edb1597a783fe6840953b0eb18cc6cc1e72a26f196d79"}, ] +decorator = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] +defusedxml = [ + {file = "defusedxml-0.6.0-py2.py3-none-any.whl", hash = "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93"}, + {file = "defusedxml-0.6.0.tar.gz", hash = "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"}, +] distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, @@ -1365,6 +1989,10 @@ docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] eradicate = [ {file = "eradicate-1.0.tar.gz", hash = "sha256:4ffda82aae6fd49dfffa777a857cb758d77502a1f2e0f54c9ac5155a39d2d01a"}, ] @@ -1466,14 +2094,54 @@ iniconfig = [ {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, ] +ipykernel = [ + {file = "ipykernel-5.3.4-py3-none-any.whl", hash = "sha256:d6fbba26dba3cebd411382bc484f7bc2caa98427ae0ddb4ab37fe8bfeb5c7dd3"}, + {file = "ipykernel-5.3.4.tar.gz", hash = "sha256:9b2652af1607986a1b231c62302d070bc0534f564c393a5d9d130db9abbbe89d"}, +] +ipython = [ + {file = "ipython-7.17.0-py3-none-any.whl", hash = "sha256:5a8f159ca8b22b9a0a1f2a28befe5ad2b703339afb58c2ffe0d7c8d7a3af5999"}, + {file = "ipython-7.17.0.tar.gz", hash = "sha256:b70974aaa2674b05eb86a910c02ed09956a33f2dd6c71afc60f0b128a77e7f28"}, +] +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-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, ] +jedi = [ + {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, + {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, +] jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] +json5 = [ + {file = "json5-0.9.5-py2.py3-none-any.whl", hash = "sha256:af1a1b9a2850c7f62c23fde18be4749b3599fd302f494eebf957e2ada6b9e42c"}, + {file = "json5-0.9.5.tar.gz", hash = "sha256:703cfee540790576b56a92e1c6aaa6c4b0d98971dc358ead83812aa4d06bdb96"}, +] +jsonschema = [ + {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, + {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, +] +jupyter-client = [ + {file = "jupyter_client-6.1.6-py3-none-any.whl", hash = "sha256:7ad9aa91505786420d77edc5f9fb170d51050c007338ba8d196f603223fd3b3a"}, + {file = "jupyter_client-6.1.6.tar.gz", hash = "sha256:b360f8d4638bc577a4656e93f86298db755f915098dc763f6fc05da0c5d7a595"}, +] +jupyter-core = [ + {file = "jupyter_core-4.6.3-py2.py3-none-any.whl", hash = "sha256:a4ee613c060fe5697d913416fc9d553599c05e4492d58fac1192c9a6844abb21"}, + {file = "jupyter_core-4.6.3.tar.gz", hash = "sha256:394fd5dd787e7c8861741880bdf8a00ce39f95de5d18e579c74b882522219e7e"}, +] +jupyterlab = [ + {file = "jupyterlab-2.2.4-py3-none-any.whl", hash = "sha256:8f4cfebf81727f6bbfc59faa7ade38493a52015736f077f815488881315a6c92"}, + {file = "jupyterlab-2.2.4.tar.gz", hash = "sha256:e9d26c4c1cf4f7760dfa9ccd3fd5ea5027ae2767f22c7766dbb2fbb5e5dfcd4b"}, +] +jupyterlab-server = [ + {file = "jupyterlab_server-1.2.0-py3-none-any.whl", hash = "sha256:55d256077bf13e5bc9e8fbd5aac51bef82f6315111cec6b712b9a5ededbba924"}, + {file = "jupyterlab_server-1.2.0.tar.gz", hash = "sha256:5431d9dde96659364b7cc877693d5d21e7b80cea7ae3959ecc2b87518e5f5d8c"}, +] 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"}, @@ -1540,6 +2208,10 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +mistune = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] more-itertools = [ {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, @@ -1564,17 +2236,85 @@ 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"}, ] +nb-black = [ + {file = "nb_black-1.0.7.tar.gz", hash = "sha256:1ca52e3a46675f6a0a6d79ac73a1f8f951bef60f919eced56173e76ab1b6d62b"}, +] +nbconvert = [ + {file = "nbconvert-5.6.1-py2.py3-none-any.whl", hash = "sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee"}, + {file = "nbconvert-5.6.1.tar.gz", hash = "sha256:21fb48e700b43e82ba0e3142421a659d7739b65568cc832a13976a77be16b523"}, +] +nbformat = [ + {file = "nbformat-5.0.7-py3-none-any.whl", hash = "sha256:ea55c9b817855e2dfcd3f66d74857342612a60b1f09653440f4a5845e6e3523f"}, + {file = "nbformat-5.0.7.tar.gz", hash = "sha256:54d4d6354835a936bad7e8182dcd003ca3dc0cedfee5a306090e04854343b340"}, +] nodeenv = [ {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, ] +notebook = [ + {file = "notebook-6.1.1-py3-none-any.whl", hash = "sha256:4cc4e44a43a83a7c2f5e85bfdbbfe1c68bed91b857741df9e593d213a6fc2d27"}, + {file = "notebook-6.1.1.tar.gz", hash = "sha256:42391d8f3b88676e774316527599e49c11f3a7e51c41035e9e44c1b58e1398d5"}, +] nox = [ {file = "nox-2020.5.24-py3-none-any.whl", hash = "sha256:c4509621fead99473a1401870e680b0aadadce5c88440f0532863595176d64c1"}, {file = "nox-2020.5.24.tar.gz", hash = "sha256:61a55705736a1a73efbd18d5b262a43d55a1176546e0eb28b29064cfcffe26c0"}, ] +numpy = [ + {file = "numpy-1.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff"}, + {file = "numpy-1.19.1-cp36-cp36m-win32.whl", hash = "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624"}, + {file = "numpy-1.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983"}, + {file = "numpy-1.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954"}, + {file = "numpy-1.19.1-cp37-cp37m-win32.whl", hash = "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b"}, + {file = "numpy-1.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055"}, + {file = "numpy-1.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd"}, + {file = "numpy-1.19.1-cp38-cp38-win32.whl", hash = "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae"}, + {file = "numpy-1.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc"}, + {file = "numpy-1.19.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1"}, + {file = "numpy-1.19.1.zip", hash = "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491"}, +] packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] +pandas = [ + {file = "pandas-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:47a03bfef80d6812c91ed6fae43f04f2fa80a4e1b82b35aa4d9002e39529e0b8"}, + {file = "pandas-1.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0210f8fe19c2667a3817adb6de2c4fd92b1b78e1975ca60c0efa908e0985cbdb"}, + {file = "pandas-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:35db623487f00d9392d8af44a24516d6cb9f274afaf73cfcfe180b9c54e007d2"}, + {file = "pandas-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:4d1a806252001c5db7caecbe1a26e49a6c23421d85a700960f6ba093112f54a1"}, + {file = "pandas-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9f61cca5262840ff46ef857d4f5f65679b82188709d0e5e086a9123791f721c8"}, + {file = "pandas-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:182a5aeae319df391c3df4740bb17d5300dcd78034b17732c12e62e6dd79e4a4"}, + {file = "pandas-1.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:40ec0a7f611a3d00d3c666c4cceb9aa3f5bf9fbd81392948a93663064f527203"}, + {file = "pandas-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16504f915f1ae424052f1e9b7cd2d01786f098fbb00fa4e0f69d42b22952d798"}, + {file = "pandas-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:fc714895b6de6803ac9f661abb316853d0cd657f5d23985222255ad76ccedc25"}, + {file = "pandas-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a15835c8409d5edc50b4af93be3377b5dd3eb53517e7f785060df1f06f6da0e2"}, + {file = "pandas-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0bc440493cf9dc5b36d5d46bbd5508f6547ba68b02a28234cd8e81fdce42744d"}, + {file = "pandas-1.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4b21d46728f8a6be537716035b445e7ef3a75dbd30bd31aa1b251323219d853e"}, + {file = "pandas-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0227e3a6e3a22c0e283a5041f1e3064d78fbde811217668bb966ed05386d8a7e"}, + {file = "pandas-1.1.0-cp38-cp38-win32.whl", hash = "sha256:ed60848caadeacecefd0b1de81b91beff23960032cded0ac1449242b506a3b3f"}, + {file = "pandas-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:60e20a4ab4d4fec253557d0fc9a4e4095c37b664f78c72af24860c8adcd07088"}, + {file = "pandas-1.1.0.tar.gz", hash = "sha256:b39508562ad0bb3f384b0db24da7d68a2608b9ddc85b1d931ccaaa92d5e45273"}, +] +pandocfilters = [ + {file = "pandocfilters-1.4.2.tar.gz", hash = "sha256:b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9"}, +] +parso = [ + {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, + {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, +] pathspec = [ {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, @@ -1587,6 +2327,14 @@ pep8-naming = [ {file = "pep8-naming-0.9.1.tar.gz", hash = "sha256:a33d38177056321a167decd6ba70b890856ba5025f0a8eca6a3eda607da93caf"}, {file = "pep8_naming-0.9.1-py2.py3-none-any.whl", hash = "sha256:45f330db8fcfb0fba57458c77385e288e7a3be1d01e8ea4268263ef677ceea5f"}, ] +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"}, @@ -1595,6 +2343,14 @@ pre-commit = [ {file = "pre_commit-2.6.0-py2.py3-none-any.whl", hash = "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626"}, {file = "pre_commit-2.6.0.tar.gz", hash = "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915"}, ] +prometheus-client = [ + {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, + {file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.6-py3-none-any.whl", hash = "sha256:683397077a64cd1f750b71c05afcfc6612a7300cb6932666531e5a54f38ea564"}, + {file = "prompt_toolkit-3.0.6.tar.gz", hash = "sha256:7630ab85a23302839a0f26b31cc24f518e6155dea1ed395ea61b42c45941b6a6"}, +] psycopg2 = [ {file = "psycopg2-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:a0984ff49e176062fcdc8a5a2a670c9bb1704a2f69548bce8f8a7bad41c661bf"}, {file = "psycopg2-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:acf56d564e443e3dea152efe972b1434058244298a94348fc518d6dd6a9fb0bb"}, @@ -1610,6 +2366,10 @@ psycopg2 = [ {file = "psycopg2-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:132efc7ee46a763e68a815f4d26223d9c679953cd190f1f218187cb60decf535"}, {file = "psycopg2-2.8.5.tar.gz", hash = "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818"}, ] +ptyprocess = [ + {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, + {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, +] py = [ {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, @@ -1618,6 +2378,10 @@ pycodestyle = [ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, ] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] pydocstyle = [ {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"}, @@ -1638,6 +2402,9 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +pyrsistent = [ + {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"}, +] pytest = [ {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, @@ -1668,6 +2435,32 @@ pytz = [ {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, ] +pywin32 = [ + {file = "pywin32-228-cp27-cp27m-win32.whl", hash = "sha256:37dc9935f6a383cc744315ae0c2882ba1768d9b06700a70f35dc1ce73cd4ba9c"}, + {file = "pywin32-228-cp27-cp27m-win_amd64.whl", hash = "sha256:11cb6610efc2f078c9e6d8f5d0f957620c333f4b23466931a247fb945ed35e89"}, + {file = "pywin32-228-cp35-cp35m-win32.whl", hash = "sha256:1f45db18af5d36195447b2cffacd182fe2d296849ba0aecdab24d3852fbf3f80"}, + {file = "pywin32-228-cp35-cp35m-win_amd64.whl", hash = "sha256:6e38c44097a834a4707c1b63efa9c2435f5a42afabff634a17f563bc478dfcc8"}, + {file = "pywin32-228-cp36-cp36m-win32.whl", hash = "sha256:ec16d44b49b5f34e99eb97cf270806fdc560dff6f84d281eb2fcb89a014a56a9"}, + {file = "pywin32-228-cp36-cp36m-win_amd64.whl", hash = "sha256:a60d795c6590a5b6baeacd16c583d91cce8038f959bd80c53bd9a68f40130f2d"}, + {file = "pywin32-228-cp37-cp37m-win32.whl", hash = "sha256:af40887b6fc200eafe4d7742c48417529a8702dcc1a60bf89eee152d1d11209f"}, + {file = "pywin32-228-cp37-cp37m-win_amd64.whl", hash = "sha256:00eaf43dbd05ba6a9b0080c77e161e0b7a601f9a3f660727a952e40140537de7"}, + {file = "pywin32-228-cp38-cp38-win32.whl", hash = "sha256:fa6ba028909cfc64ce9e24bcf22f588b14871980d9787f1e2002c99af8f1850c"}, + {file = "pywin32-228-cp38-cp38-win_amd64.whl", hash = "sha256:9b3466083f8271e1a5eb0329f4e0d61925d46b40b195a33413e0905dccb285e8"}, + {file = "pywin32-228-cp39-cp39-win32.whl", hash = "sha256:ed74b72d8059a6606f64842e7917aeee99159ebd6b8d6261c518d002837be298"}, + {file = "pywin32-228-cp39-cp39-win_amd64.whl", hash = "sha256:8319bafdcd90b7202c50d6014efdfe4fde9311b3ff15fd6f893a45c0868de203"}, +] +pywinpty = [ + {file = "pywinpty-0.5.7-cp27-cp27m-win32.whl", hash = "sha256:b358cb552c0f6baf790de375fab96524a0498c9df83489b8c23f7f08795e966b"}, + {file = "pywinpty-0.5.7-cp27-cp27m-win_amd64.whl", hash = "sha256:1e525a4de05e72016a7af27836d512db67d06a015aeaf2fa0180f8e6a039b3c2"}, + {file = "pywinpty-0.5.7-cp35-cp35m-win32.whl", hash = "sha256:2740eeeb59297593a0d3f762269b01d0285c1b829d6827445fcd348fb47f7e70"}, + {file = "pywinpty-0.5.7-cp35-cp35m-win_amd64.whl", hash = "sha256:33df97f79843b2b8b8bc5c7aaf54adec08cc1bae94ee99dfb1a93c7a67704d95"}, + {file = "pywinpty-0.5.7-cp36-cp36m-win32.whl", hash = "sha256:e854211df55d107f0edfda8a80b39dfc87015bef52a8fe6594eb379240d81df2"}, + {file = "pywinpty-0.5.7-cp36-cp36m-win_amd64.whl", hash = "sha256:dbd838de92de1d4ebf0dce9d4d5e4fc38d0b7b1de837947a18b57a882f219139"}, + {file = "pywinpty-0.5.7-cp37-cp37m-win32.whl", hash = "sha256:5fb2c6c6819491b216f78acc2c521b9df21e0f53b9a399d58a5c151a3c4e2a2d"}, + {file = "pywinpty-0.5.7-cp37-cp37m-win_amd64.whl", hash = "sha256:dd22c8efacf600730abe4a46c1388355ce0d4ab75dc79b15d23a7bd87bf05b48"}, + {file = "pywinpty-0.5.7-cp38-cp38-win_amd64.whl", hash = "sha256:8fc5019ff3efb4f13708bd3b5ad327589c1a554cb516d792527361525a7cb78c"}, + {file = "pywinpty-0.5.7.tar.gz", hash = "sha256:2d7e9c881638a72ffdca3f5417dd1563b60f603e1b43e5895674c2a1b01f95a0"}, +] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, @@ -1681,6 +2474,36 @@ pyyaml = [ {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] +pyzmq = [ + {file = "pyzmq-19.0.2-cp27-cp27m-macosx_10_9_intel.whl", hash = "sha256:59f1e54627483dcf61c663941d94c4af9bf4163aec334171686cdaee67974fe5"}, + {file = "pyzmq-19.0.2-cp27-cp27m-win32.whl", hash = "sha256:c36ffe1e5aa35a1af6a96640d723d0d211c5f48841735c2aa8d034204e87eb87"}, + {file = "pyzmq-19.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:0a422fc290d03958899743db091f8154958410fc76ce7ee0ceb66150f72c2c97"}, + {file = "pyzmq-19.0.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c20dd60b9428f532bc59f2ef6d3b1029a28fc790d408af82f871a7db03e722ff"}, + {file = "pyzmq-19.0.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d46fb17f5693244de83e434648b3dbb4f4b0fec88415d6cbab1c1452b6f2ae17"}, + {file = "pyzmq-19.0.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:f1a25a61495b6f7bb986accc5b597a3541d9bd3ef0016f50be16dbb32025b302"}, + {file = "pyzmq-19.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ab0d01148d13854de716786ca73701012e07dff4dfbbd68c4e06d8888743526e"}, + {file = "pyzmq-19.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:720d2b6083498a9281eaee3f2927486e9fe02cd16d13a844f2e95217f243efea"}, + {file = "pyzmq-19.0.2-cp35-cp35m-win32.whl", hash = "sha256:29d51279060d0a70f551663bc592418bcad7f4be4eea7b324f6dd81de05cb4c1"}, + {file = "pyzmq-19.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:5120c64646e75f6db20cc16b9a94203926ead5d633de9feba4f137004241221d"}, + {file = "pyzmq-19.0.2-cp36-cp36m-macosx_10_9_intel.whl", hash = "sha256:8a6ada5a3f719bf46a04ba38595073df8d6b067316c011180102ba2a1925f5b5"}, + {file = "pyzmq-19.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fa411b1d8f371d3a49d31b0789eb6da2537dadbb2aef74a43aa99a78195c3f76"}, + {file = "pyzmq-19.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:00dca814469436455399660247d74045172955459c0bd49b54a540ce4d652185"}, + {file = "pyzmq-19.0.2-cp36-cp36m-win32.whl", hash = "sha256:046b92e860914e39612e84fa760fc3f16054d268c11e0e25dcb011fb1bc6a075"}, + {file = "pyzmq-19.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99cc0e339a731c6a34109e5c4072aaa06d8e32c0b93dc2c2d90345dd45fa196c"}, + {file = "pyzmq-19.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e36f12f503511d72d9bdfae11cadbadca22ff632ff67c1b5459f69756a029c19"}, + {file = "pyzmq-19.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c40fbb2b9933369e994b837ee72193d6a4c35dfb9a7c573257ef7ff28961272c"}, + {file = "pyzmq-19.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5d9fc809aa8d636e757e4ced2302569d6e60e9b9c26114a83f0d9d6519c40493"}, + {file = "pyzmq-19.0.2-cp37-cp37m-win32.whl", hash = "sha256:3fa6debf4bf9412e59353defad1f8035a1e68b66095a94ead8f7a61ae90b2675"}, + {file = "pyzmq-19.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:73483a2caaa0264ac717af33d6fb3f143d8379e60a422730ee8d010526ce1913"}, + {file = "pyzmq-19.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:36ab114021c0cab1a423fe6689355e8f813979f2c750968833b318c1fa10a0fd"}, + {file = "pyzmq-19.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8b66b94fe6243d2d1d89bca336b2424399aac57932858b9a30309803ffc28112"}, + {file = "pyzmq-19.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:654d3e06a4edc566b416c10293064732516cf8871a4522e0a2ba00cc2a2e600c"}, + {file = "pyzmq-19.0.2-cp38-cp38-win32.whl", hash = "sha256:276ad604bffd70992a386a84bea34883e696a6b22e7378053e5d3227321d9702"}, + {file = "pyzmq-19.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:09d24a80ccb8cbda1af6ed8eb26b005b6743e58e9290566d2a6841f4e31fa8e0"}, + {file = "pyzmq-19.0.2-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:c1a31cd42905b405530e92bdb70a8a56f048c8a371728b8acf9d746ecd4482c0"}, + {file = "pyzmq-19.0.2-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a7e7f930039ee0c4c26e4dfee015f20bd6919cd8b97c9cd7afbde2923a5167b6"}, + {file = "pyzmq-19.0.2.tar.gz", hash = "sha256:296540a065c8c21b26d63e3cea2d1d57902373b16e4256afe46422691903a438"}, +] regex = [ {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, @@ -1711,6 +2534,10 @@ requests = [ restructuredtext-lint = [ {file = "restructuredtext_lint-1.3.1.tar.gz", hash = "sha256:470e53b64817211a42805c3a104d2216f6f5834b22fe7adb637d1de4d6501fb8"}, ] +send2trash = [ + {file = "Send2Trash-1.5.0-py3-none-any.whl", hash = "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"}, + {file = "Send2Trash-1.5.0.tar.gz", hash = "sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2"}, +] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, @@ -1789,14 +2616,37 @@ stevedore = [ {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, ] +terminado = [ + {file = "terminado-0.8.3-py2.py3-none-any.whl", hash = "sha256:a43dcb3e353bc680dd0783b1d9c3fc28d529f190bc54ba9a229f72fe6e7a54d7"}, + {file = "terminado-0.8.3.tar.gz", hash = "sha256:4804a774f802306a7d9af7322193c5390f1da0abb429e082a10ef1d46e6fb2c2"}, +] testfixtures = [ {file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"}, {file = "testfixtures-6.14.1.tar.gz", hash = "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366"}, ] +testpath = [ + {file = "testpath-0.4.4-py2.py3-none-any.whl", hash = "sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4"}, + {file = "testpath-0.4.4.tar.gz", hash = "sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e"}, +] toml = [ {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, ] +tornado = [ + {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, + {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, + {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, + {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, + {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, + {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, + {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, + {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, + {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, +] +traitlets = [ + {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, + {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, +] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, @@ -1833,6 +2683,14 @@ virtualenv = [ {file = "virtualenv-20.0.30-py2.py3-none-any.whl", hash = "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e"}, {file = "virtualenv-20.0.30.tar.gz", hash = "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5"}, ] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] wemake-python-styleguide = [ {file = "wemake-python-styleguide-0.14.1.tar.gz", hash = "sha256:e13dc580fa56b7b548de8da170bccb8ddff2d4ab026ca987db8a9893bf8a7b5b"}, {file = "wemake_python_styleguide-0.14.1-py3-none-any.whl", hash = "sha256:73a501e0547275287a2b926515c000cc25026a8bceb9dcc1bf73ef85a223a3c6"}, diff --git a/pyproject.toml b/pyproject.toml index 505526e..7752a23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,12 +27,30 @@ repository = "https://github.com/webartifex/urban-meal-delivery" [tool.poetry.dependencies] python = "^3.8" +# Package => code developed in *.py files and packaged under src/urban_meal_delivery alembic = "^1.4.2" click = "^7.1.2" psycopg2 = "^2.8.5" # adapter for PostgreSQL python-dotenv = "^0.14.0" sqlalchemy = "^1.3.18" +# Jupyter Lab => notebooks with analyses using the developed package +# IMPORTANT: must be kept in sync with the "research" extra below +jupyterlab = { version="^2.2.2", optional=true } +nb_black = { version="^1.0.7", optional=true } +numpy = { version="^1.19.1", optional=true } +pandas = { version="^1.1.0", optional=true } +pytz = { version="^2020.1", optional=true } + +[tool.poetry.extras] +research = [ + "jupyterlab", + "nb_black", + "numpy", + "pandas", + "pytz", +] + [tool.poetry.dev-dependencies] # Task Runners nox = "^2020.5.24" From 79f0ddf0fead0fe0162e7d1e18f60f481a9cc51c Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 11 Aug 2020 11:02:09 +0200 Subject: [PATCH 10/14] Add installation and contributing info --- README.md | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2b671fe..ac69362 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,40 @@ operating in France from January 2016 to January 2017. The goal is to optimize the platform's delivery process involving independent couriers. + +## Structure + The analysis is structured into three aspects that iteratively build on each other. -## Real-time Demand Forecasting +### Real-time Demand Forecasting -## Predictive Routing +### Predictive Routing -## Shift & Capacity Planning +### Shift & Capacity Planning + + +## Installation & Contribution + +To play with the code developed for the analyses, +you can clone the project with [git](https://git-scm.com/) +and install the contained `urban-meal-delivery` package +and all its dependencies +in a [virtual environment](https://docs.python.org/3/tutorial/venv.html) +with [poetry](https://python-poetry.org/docs/): + +`git clone https://github.com/webartifex/urban-meal-delivery.git` + +and + +`poetry install --extras research` + +The `--extras` option is necessary as the non-develop dependencies +are structured in the [pyproject.toml](https://github.com/webartifex/urban-meal-delivery/blob/develop/pyproject.toml) file +into dependencies related to only the `urban-meal-delivery` source code package +and dependencies used to run the [Jupyter](https://jupyter.org/) environment +with the analyses. + +Contributions are welcome. +Use the [issues](https://github.com/webartifex/urban-meal-delivery/issues) tab. +The project is licensed under the [MIT license](https://github.com/webartifex/urban-meal-delivery/blob/develop/LICENSE.txt). From db119ea776cf400dfe867ae017599d18ffb14222 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 11 Aug 2020 13:55:55 +0200 Subject: [PATCH 11/14] Adjust the branch reference fixer's logic - change references to temporary branches (e.g., "release-*" and "publish") to point to the 'main' branch - add --branch=BRANCH_NAME option to the nox session so that one can pass in a target branch to make all references point to - run "fix-branch-references" as the first pre-commit hook as it fails the fastest - bug fix: allow dots in branch references (e.g., "release-0.1.0") --- .pre-commit-config.yaml | 12 ++++++------ noxfile.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 987b01d..828e482 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,12 @@ repos: # Run the local formatting, linting, and testing tool chains. - repo: local hooks: + - id: local-fix-branch-references + name: Check for wrong branch references + entry: poetry run nox -s fix-branch-references -- + language: system + stages: [commit] + types: [text] - id: local-format name: Format the source files entry: poetry run nox -s format -- @@ -16,12 +22,6 @@ repos: language: system stages: [commit] types: [python] - - id: local-fix-branch-references - name: Adjust the branch references - entry: poetry run nox -s fix-branch-references -- - language: system - stages: [commit] - types: [text] - id: local-test-suite name: Run the entire test suite entry: poetry run nox -s test-suite -- diff --git a/noxfile.py b/noxfile.py index 49e49e0..06cdd7f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -328,7 +328,7 @@ def test_suite(session): @nox.session(name='fix-branch-references', python=PYTHON, venv_backend='none') -def fix_branch_references(_): # noqa:WPS210 +def fix_branch_references(session): # noqa:WPS210 """Replace branch references with the current branch. Intended to be run as a pre-commit hook. @@ -337,12 +337,20 @@ def fix_branch_references(_): # noqa:WPS210 on github.com or nbviewer.jupyter.org that contain branch labels. This task rewrites these links such that they contain the branch reference - of the current branch. + of the current branch. If the branch is only a temporary one that is to be + merged into the 'main' branch, all references are adjusted to 'main' as well. + + This task may be called with one positional argument that is interpreted + as the branch to which all references are changed into. + The format must be "--branch=BRANCH_NAME". """ # Adjust this to add/remove glob patterns # whose links are re-written. paths = ['*.md', '**/*.md', '**/*.ipynb'] + # Get the branch git is currently on. + # This is the branch to which all references are changed into + # if none of the two exceptions below apply. branch = ( subprocess.check_output( # noqa:S603 ('git', 'rev-parse', '--abbrev-ref', 'HEAD'), @@ -350,19 +358,33 @@ def fix_branch_references(_): # noqa:WPS210 .decode() .strip() ) + # If the current branch is only a temporary one that is to be merged + # into 'main', we adjust all branch references to 'main' as well. + if branch.startswith('release') or branch.startswith('research'): + branch = 'main' + # If a "--branch=BRANCH_NAME" argument is passed in + # as the only positional argument, we use BRANCH_NAME. + # Note: The --branch is required as session.posargs contains + # the staged files passed in by pre-commit in most cases. + if session.posargs and len(session.posargs) == 1: + match = re.match( + pattern=r'^--branch=([\w\.-]+)$', string=session.posargs[0].strip(), + ) + if match: + branch = match.groups()[0] rewrites = [ { 'name': 'github', 'pattern': re.compile( - fr'((((http)|(https))://github\.com/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w-]+)/)', # noqa:E501 + fr'((((http)|(https))://github\.com/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w\.-]+)/)', # noqa:E501 ), 'replacement': fr'\2{branch}/', }, { 'name': 'nbviewer', 'pattern': re.compile( - fr'((((http)|(https))://nbviewer\.jupyter\.org/github/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w-]+)/)', # noqa:E501 + fr'((((http)|(https))://nbviewer\.jupyter\.org/github/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w\.-]+)/)', # noqa:E501 ), 'replacement': fr'\2{branch}/', }, From a67805fcff80eddb218a9c005655cf17e4d32fbf Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 30 Sep 2020 11:54:23 +0200 Subject: [PATCH 12/14] Upgrade isort to v5.5.4 --- noxfile.py | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 06cdd7f..6a9620d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -527,11 +527,11 @@ def _install_packages(session: Session, *packages_or_pip_args: str, **kwargs) -> # TODO (isort): Remove this fix after -# upgrading to isort ^5.3.0 in pyproject.toml. +# upgrading to isort ^5.5.4 in pyproject.toml. @contextlib.contextmanager def _isort_fix(session): - """Temporarily upgrade to isort 5.3.0.""" - session.install('isort==5.3.0') + """Temporarily upgrade to isort 5.5.4.""" + session.install('isort==5.5.4') try: yield finally: diff --git a/pyproject.toml b/pyproject.toml index 7752a23..9b5b456 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ pre-commit = "^2.6.0" # Code Formatters autoflake = "^1.3.1" black = "^19.10b0" -isort = "^4.3.21" # TODO (isort): not ^5.2.2 due to pylint and wemake-python-styleguide +isort = "^4.3.21" # TODO (isort): not ^5.5.4 due to wemake-python-styleguide # (Static) Code Analyzers flake8 = "^3.8.3" From deeba63fbda1ba68cd93fe8f86fc8a56d4e2eeb5 Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 30 Sep 2020 12:10:23 +0200 Subject: [PATCH 13/14] Pin the dependencies ... .. after upgrading a couple of packages --- poetry.lock | 675 ++++++++++++++++++++++++++++------------------------ 1 file changed, 368 insertions(+), 307 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4f5dafa..9fa86ac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,7 +12,7 @@ description = "A database migration tool for SQLAlchemy." name = "alembic" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.2" +version = "1.4.3" [package.dependencies] Mako = "*" @@ -43,7 +43,7 @@ description = "Bash tab completion for argparse" name = "argcomplete" optional = false python-versions = "*" -version = "1.12.0" +version = "1.12.1" [package.extras] test = ["coverage", "flake8", "pexpect", "wheel"] @@ -97,6 +97,14 @@ lazy-object-proxy = ">=1.4.0,<1.5.0" six = ">=1.12,<2.0" wrapt = ">=1.11,<2.0" +[[package]] +category = "main" +description = "Async generators and context managers for Python 3.5+" +name = "async-generator" +optional = true +python-versions = ">=3.5" +version = "1.10" + [[package]] category = "dev" description = "Atomic file writes." @@ -112,13 +120,13 @@ description = "Classes Without Boilerplate" name = "attrs" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.3.0" +version = "20.2.0" [package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] [[package]] category = "dev" @@ -126,7 +134,7 @@ description = "Removes unused imports and unused variables" name = "autoflake" optional = false python-versions = "*" -version = "1.3.1" +version = "1.4" [package.dependencies] pyflakes = ">=1.1.0" @@ -191,7 +199,7 @@ description = "An easy safelist-based HTML-sanitizing tool." name = "bleach" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "3.1.5" +version = "3.2.1" [package.dependencies] packaging = "*" @@ -212,7 +220,7 @@ description = "Foreign Function Interface for Python calling C code." name = "cffi" optional = true python-versions = "*" -version = "1.14.1" +version = "1.14.3" [package.dependencies] pycparser = "*" @@ -267,7 +275,7 @@ description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2.1" +version = "5.3" [package.extras] toml = ["toml"] @@ -278,7 +286,7 @@ description = "A utility for ensuring Google-style docstrings stay up to date wi name = "darglint" optional = false python-versions = ">=3.5,<4.0" -version = "1.5.2" +version = "1.5.4" [[package]] category = "main" @@ -355,7 +363,7 @@ description = "Flake8 Type Annotation Checks" name = "flake8-annotations" optional = false python-versions = ">=3.6.1,<4.0.0" -version = "2.3.0" +version = "2.4.1" [package.dependencies] flake8 = ">=3.7,<3.9" @@ -525,7 +533,7 @@ description = "A flake8 plugin checking common style issues or inconsistencies w name = "flake8-pytest-style" optional = false python-versions = ">=3.6,<4.0" -version = "1.2.3" +version = "1.3.0" [package.dependencies] flake8-plugin-utils = ">=1.3.1,<2.0.0" @@ -581,7 +589,7 @@ description = "Python Git Library" name = "gitpython" optional = false python-versions = ">=3.4" -version = "3.1.7" +version = "3.1.8" [package.dependencies] gitdb = ">=4.0.1,<5" @@ -592,7 +600,7 @@ description = "File identification library for Python" name = "identify" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "1.4.25" +version = "1.5.5" [package.extras] license = ["editdistance"] @@ -645,7 +653,7 @@ description = "IPython: Productive Interactive Computing" name = "ipython" optional = true python-versions = ">=3.7" -version = "7.17.0" +version = "7.18.1" [package.dependencies] appnope = "*" @@ -653,7 +661,7 @@ backcall = "*" colorama = "*" decorator = "*" jedi = ">=0.10" -pexpect = "*" +pexpect = ">4.3" pickleshare = "*" prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" pygments = "*" @@ -757,7 +765,7 @@ description = "Jupyter protocol implementation and client libraries" name = "jupyter-client" optional = true python-versions = ">=3.5" -version = "6.1.6" +version = "6.1.7" [package.dependencies] jupyter-core = ">=4.6.0" @@ -767,7 +775,7 @@ tornado = ">=4.1" traitlets = "*" [package.extras] -test = ["async-generator", "ipykernel", "ipython", "mock", "pytest", "pytest-asyncio", "pytest-timeout"] +test = ["ipykernel", "ipython", "mock", "pytest", "pytest-asyncio", "async-generator", "pytest-timeout"] [[package]] category = "main" @@ -787,7 +795,7 @@ description = "The JupyterLab notebook server extension." name = "jupyterlab" optional = true python-versions = ">=3.5" -version = "2.2.4" +version = "2.2.8" [package.dependencies] jinja2 = ">=2.10" @@ -799,6 +807,17 @@ tornado = "<6.0.0 || >6.0.0,<6.0.1 || >6.0.1,<6.0.2 || >6.0.2" docs = ["jsx-lexer", "recommonmark", "sphinx", "sphinx-rtd-theme", "sphinx-copybutton"] test = ["pytest", "pytest-check-links", "requests", "wheel", "virtualenv"] +[[package]] +category = "main" +description = "Pygments theme using JupyterLab CSS variables" +name = "jupyterlab-pygments" +optional = true +python-versions = "*" +version = "0.1.2" + +[package.dependencies] +pygments = ">=2.4.1,<3" + [[package]] category = "main" description = "JupyterLab Server" @@ -864,14 +883,6 @@ optional = true python-versions = "*" version = "0.8.4" -[[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" -optional = false -python-versions = ">=3.5" -version = "8.4.0" - [[package]] category = "dev" description = "Optional static typing for Python" @@ -907,13 +918,33 @@ version = "1.0.7" [package.dependencies] ipython = "*" +[[package]] +category = "main" +description = "A client library for executing notebooks. Formally nbconvert's ExecutePreprocessor." +name = "nbclient" +optional = true +python-versions = ">=3.6" +version = "0.5.0" + +[package.dependencies] +async-generator = "*" +jupyter-client = ">=6.1.5" +nbformat = ">=5.0" +nest-asyncio = "*" +traitlets = ">=4.2" + +[package.extras] +dev = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "check-manifest", "flake8", "mypy", "tox", "bumpversion", "xmltodict", "pip (>=18.1)", "wheel (>=0.31.0)", "setuptools (>=38.6.0)", "twine (>=1.11.0)", "black"] +sphinx = ["Sphinx (>=1.7)", "sphinx-book-theme", "mock", "moto", "myst-parser"] +test = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "check-manifest", "flake8", "mypy", "tox", "bumpversion", "xmltodict", "pip (>=18.1)", "wheel (>=0.31.0)", "setuptools (>=38.6.0)", "twine (>=1.11.0)", "black"] + [[package]] category = "main" description = "Converting Jupyter Notebooks" name = "nbconvert" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.6.1" +python-versions = ">=3.6" +version = "6.0.6" [package.dependencies] bleach = "*" @@ -921,19 +952,21 @@ defusedxml = "*" entrypoints = ">=0.2.2" jinja2 = ">=2.4" jupyter-core = "*" +jupyterlab-pygments = "*" mistune = ">=0.8.1,<2" +nbclient = ">=0.5.0,<0.6.0" nbformat = ">=4.4" pandocfilters = ">=1.4.1" -pygments = "*" +pygments = ">=2.4.1" testpath = "*" traitlets = ">=4.2" [package.extras] -all = ["pytest", "pytest-cov", "ipykernel", "jupyter-client (>=5.3.1)", "ipywidgets (>=7)", "pebble", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "sphinxcontrib-github-alt", "ipython", "mock"] -docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "sphinxcontrib-github-alt", "ipython", "jupyter-client (>=5.3.1)"] -execute = ["jupyter-client (>=5.3.1)"] +all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (0.2.2)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] +docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] serve = ["tornado (>=4.0)"] -test = ["pytest", "pytest-cov", "ipykernel", "jupyter-client (>=5.3.1)", "ipywidgets (>=7)", "pebble", "mock"] +test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (0.2.2)"] +webpdf = ["pyppeteer (0.2.2)"] [[package]] category = "main" @@ -952,13 +985,21 @@ traitlets = ">=4.1" [package.extras] test = ["pytest", "pytest-cov", "testpath"] +[[package]] +category = "main" +description = "Patch asyncio to allow nested event loops" +name = "nest-asyncio" +optional = true +python-versions = ">=3.5" +version = "1.4.1" + [[package]] category = "dev" description = "Node.js virtual environment builder" name = "nodeenv" optional = false python-versions = "*" -version = "1.4.0" +version = "1.5.0" [[package]] category = "main" @@ -966,7 +1007,7 @@ description = "A web-based notebook environment for interactive computing" name = "notebook" optional = true python-versions = ">=3.5" -version = "6.1.1" +version = "6.1.4" [package.dependencies] Send2Trash = "*" @@ -994,7 +1035,7 @@ description = "Flexible test automation." name = "nox" optional = false python-versions = ">=3.5" -version = "2020.5.24" +version = "2020.8.22" [package.dependencies] argcomplete = ">=1.9.4,<2.0" @@ -1011,7 +1052,7 @@ description = "NumPy is the fundamental package for array computing with Python. name = "numpy" optional = true python-versions = ">=3.6" -version = "1.19.1" +version = "1.19.2" [[package]] category = "main" @@ -1031,7 +1072,7 @@ description = "Powerful data structures for data analysis, time series, and stat name = "pandas" optional = true python-versions = ">=3.6.1" -version = "1.1.0" +version = "1.1.2" [package.dependencies] numpy = ">=1.15.4" @@ -1073,8 +1114,8 @@ category = "dev" description = "Python Build Reasonableness" name = "pbr" optional = false -python-versions = "*" -version = "5.4.5" +python-versions = ">=2.6" +version = "5.5.0" [[package]] category = "dev" @@ -1124,7 +1165,7 @@ description = "A framework for managing and maintaining multi-language pre-commi name = "pre-commit" optional = false python-versions = ">=3.6.1" -version = "2.6.0" +version = "2.7.1" [package.dependencies] cfgv = ">=2.0.0" @@ -1151,7 +1192,7 @@ description = "Library for building powerful interactive command lines in Python name = "prompt-toolkit" optional = true python-versions = ">=3.6.1" -version = "3.0.6" +version = "3.0.7" [package.dependencies] wcwidth = "*" @@ -1162,7 +1203,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" name = "psycopg2" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "2.8.5" +version = "2.8.6" [[package]] category = "main" @@ -1203,7 +1244,7 @@ description = "Python docstring style checker" name = "pydocstyle" optional = false python-versions = ">=3.5" -version = "5.0.2" +version = "5.1.1" [package.dependencies] snowballstemmer = "*" @@ -1222,7 +1263,7 @@ description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false python-versions = ">=3.5" -version = "2.6.1" +version = "2.7.1" [[package]] category = "dev" @@ -1230,12 +1271,12 @@ description = "python code static checker" name = "pylint" optional = false python-versions = ">=3.5.*" -version = "2.5.3" +version = "2.6.0" [package.dependencies] astroid = ">=2.4.0,<=2.5" colorama = "*" -isort = ">=4.2.5,<5" +isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" toml = ">=0.7.1" @@ -1252,11 +1293,8 @@ category = "main" description = "Persistent/Functional/Immutable data structures" name = "pyrsistent" optional = true -python-versions = "*" -version = "0.16.0" - -[package.dependencies] -six = "*" +python-versions = ">=3.5" +version = "0.17.3" [[package]] category = "dev" @@ -1264,14 +1302,13 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=3.5" -version = "6.0.1" +version = "6.1.0" [package.dependencies] atomicwrites = ">=1.0" attrs = ">=17.4.0" colorama = "*" iniconfig = "*" -more-itertools = ">=4.0.0" packaging = "*" pluggy = ">=0.12,<1.0" py = ">=1.8.2" @@ -1287,7 +1324,7 @@ description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.0" +version = "2.10.1" [package.dependencies] coverage = ">=4.4" @@ -1385,7 +1422,7 @@ description = "Alternative regular expression module, to replace re." name = "regex" optional = false python-versions = "*" -version = "2020.7.14" +version = "2020.9.27" [[package]] category = "main" @@ -1454,7 +1491,7 @@ description = "Python documentation generator" name = "sphinx" optional = false python-versions = ">=3.5" -version = "3.2.0" +version = "3.2.1" [package.dependencies] Jinja2 = ">=2.3" @@ -1572,7 +1609,7 @@ description = "Database Abstraction Library" name = "sqlalchemy" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.3.18" +version = "1.3.19" [package.extras] mssql = ["pyodbc"] @@ -1592,18 +1629,18 @@ description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false python-versions = ">=3.6" -version = "3.2.0" +version = "3.2.2" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] category = "main" -description = "Terminals served to xterm.js using Tornado websockets" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." name = "terminado" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.8.3" +python-versions = ">=3.6" +version = "0.9.1" [package.dependencies] ptyprocess = "*" @@ -1616,7 +1653,7 @@ description = "A collection of helpers and mock objects for unit tests and doc t name = "testfixtures" optional = false python-versions = "*" -version = "6.14.1" +version = "6.14.2" [package.extras] build = ["setuptools-git", "wheel", "twine"] @@ -1652,19 +1689,17 @@ version = "6.0.4" [[package]] category = "main" -description = "Traitlets Python config system" +description = "Traitlets Python configuration system" name = "traitlets" optional = true -python-versions = "*" -version = "4.3.3" +python-versions = ">=3.7" +version = "5.0.4" [package.dependencies] -decorator = "*" ipython-genutils = "*" -six = "*" [package.extras] -test = ["pytest", "mock"] +test = ["pytest"] [[package]] category = "dev" @@ -1680,7 +1715,7 @@ description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4.2" +version = "3.7.4.3" [[package]] category = "main" @@ -1701,7 +1736,7 @@ description = "Virtual Python Environment builder" name = "virtualenv" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "20.0.30" +version = "20.0.31" [package.dependencies] appdirs = ">=1.4.3,<2" @@ -1804,7 +1839,8 @@ alabaster = [ {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] alembic = [ - {file = "alembic-1.4.2.tar.gz", hash = "sha256:035ab00497217628bf5d0be82d664d8713ab13d37b630084da8e1f98facf4dbf"}, + {file = "alembic-1.4.3-py2.py3-none-any.whl", hash = "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c"}, + {file = "alembic-1.4.3.tar.gz", hash = "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245"}, ] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, @@ -1815,8 +1851,8 @@ appnope = [ {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, ] argcomplete = [ - {file = "argcomplete-1.12.0-py2.py3-none-any.whl", hash = "sha256:91dc7f9c7f6281d5a0dce5e73d2e33283aaef083495c13974a7dd197a1cdc949"}, - {file = "argcomplete-1.12.0.tar.gz", hash = "sha256:2fbe5ed09fd2c1d727d4199feca96569a5b50d44c71b16da9c742201f7cc295c"}, + {file = "argcomplete-1.12.1-py2.py3-none-any.whl", hash = "sha256:5cd1ac4fc49c29d6016fc2cc4b19a3c08c3624544503495bf25989834c443898"}, + {file = "argcomplete-1.12.1.tar.gz", hash = "sha256:849c2444c35bb2175aea74100ca5f644c29bf716429399c0f2203bb5d9a8e4e6"}, ] argon2-cffi = [ {file = "argon2-cffi-20.1.0.tar.gz", hash = "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d"}, @@ -1848,16 +1884,20 @@ astroid = [ {file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"}, {file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"}, ] +async-generator = [ + {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, + {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, +] 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-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, + {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, + {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, ] autoflake = [ - {file = "autoflake-1.3.1.tar.gz", hash = "sha256:680cb9dade101ed647488238ccb8b8bfb4369b53d58ba2c8cdf7d5d54e01f95b"}, + {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, ] babel = [ {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, @@ -1876,42 +1916,50 @@ black = [ {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, ] bleach = [ - {file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"}, - {file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"}, + {file = "bleach-3.2.1-py2.py3-none-any.whl", hash = "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd"}, + {file = "bleach-3.2.1.tar.gz", hash = "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080"}, ] certifi = [ {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, ] cffi = [ - {file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"}, - {file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"}, - {file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"}, - {file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"}, - {file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"}, - {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"}, - {file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"}, - {file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"}, - {file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"}, - {file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"}, - {file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"}, - {file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"}, - {file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"}, - {file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"}, - {file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"}, - {file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"}, - {file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"}, - {file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"}, - {file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"}, - {file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"}, - {file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"}, - {file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"}, - {file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"}, - {file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"}, - {file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"}, - {file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"}, - {file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"}, - {file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"}, + {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, + {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, + {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, + {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, + {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, + {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, + {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, + {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, + {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, + {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, + {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, + {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, + {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, + {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, + {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, + {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, + {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, + {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, + {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, ] cfgv = [ {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, @@ -1934,44 +1982,44 @@ colorlog = [ {file = "colorlog-4.2.1.tar.gz", hash = "sha256:75e55822c3a3387d721579241e776de2cf089c9ef9528b1f09e8b04d403ad118"}, ] coverage = [ - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, - {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, - {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, - {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, - {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, - {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, - {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, - {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, - {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, - {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, - {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, - {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, - {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, - {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, - {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, - {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, - {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, - {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, - {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, + {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, + {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, + {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, + {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, + {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, + {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, + {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, + {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, + {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, + {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, + {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, + {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, + {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, + {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, + {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, + {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, + {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, + {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, ] darglint = [ - {file = "darglint-1.5.2-py3-none-any.whl", hash = "sha256:049a98cf3aec8cf6ea344a863c68112d80b7f8de214459b5fa6853371f89c3e7"}, - {file = "darglint-1.5.2.tar.gz", hash = "sha256:6b9461f96694c2cf1d8edb1597a783fe6840953b0eb18cc6cc1e72a26f196d79"}, + {file = "darglint-1.5.4-py3-none-any.whl", hash = "sha256:e58ff63f0f29a4dc8f9c1e102c7d00539290567d72feb74b7b9d5f8302992b8d"}, + {file = "darglint-1.5.4.tar.gz", hash = "sha256:7ebaafc8559d0db7735b6e15904ee5cca4be56fa85eac21c025c328278c6317a"}, ] decorator = [ {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, @@ -2005,8 +2053,8 @@ flake8 = [ {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, ] flake8-annotations = [ - {file = "flake8-annotations-2.3.0.tar.gz", hash = "sha256:8d18db74a750dd97f40b483cc3ef80d07d03f687525bad8fd83365dcd3bfd414"}, - {file = "flake8_annotations-2.3.0-py3-none-any.whl", hash = "sha256:7816a5d8f65ffdf37b8e21e5b17e0fd1e492aa92638573276de066e889a22b26"}, + {file = "flake8-annotations-2.4.1.tar.gz", hash = "sha256:0bcebb0792f1f96d617ded674dca7bf64181870bfe5dace353a1483551f8e5f1"}, + {file = "flake8_annotations-2.4.1-py3-none-any.whl", hash = "sha256:bebd11a850f6987a943ce8cdff4159767e0f5f89b3c88aca64680c2175ee02df"}, ] flake8-bandit = [ {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, @@ -2057,8 +2105,8 @@ flake8-polyfill = [ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] flake8-pytest-style = [ - {file = "flake8-pytest-style-1.2.3.tar.gz", hash = "sha256:48e5ed79ab33846112331bbf51353c568d8a5f515de49c6cd9dfe5fecdcaa6f3"}, - {file = "flake8_pytest_style-1.2.3-py3-none-any.whl", hash = "sha256:fa6e8706b814b1eedf8f7aa62e26bfc03d0e5178a25d828218165be6c8e52570"}, + {file = "flake8-pytest-style-1.3.0.tar.gz", hash = "sha256:d141476de2d1a31e491c2090ba7d1e32980b11a2c12e8aa3b3cc844571b19bfa"}, + {file = "flake8_pytest_style-1.3.0-py3-none-any.whl", hash = "sha256:5a0bfb30696eb97473bb2078834794e9491848f975f680bdcb0554e5b4efbbfc"}, ] flake8-quotes = [ {file = "flake8-quotes-2.1.2.tar.gz", hash = "sha256:c844c9592940c8926c60f00bc620808912ff2acd34923ab5338f3a5ca618a331"}, @@ -2075,12 +2123,12 @@ gitdb = [ {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, ] gitpython = [ - {file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"}, - {file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"}, + {file = "GitPython-3.1.8-py3-none-any.whl", hash = "sha256:1858f4fd089abe92ae465f01d5aaaf55e937eca565fb2c1fce35a51b5f85c910"}, + {file = "GitPython-3.1.8.tar.gz", hash = "sha256:080bf8e2cf1a2b907634761c2eaefbe83b69930c94c66ad11b65a8252959f912"}, ] identify = [ - {file = "identify-1.4.25-py2.py3-none-any.whl", hash = "sha256:ccd88716b890ecbe10920659450a635d2d25de499b9a638525a48b48261d989b"}, - {file = "identify-1.4.25.tar.gz", hash = "sha256:110ed090fec6bce1aabe3c72d9258a9de82207adeaa5a05cd75c635880312f9a"}, + {file = "identify-1.5.5-py2.py3-none-any.whl", hash = "sha256:da683bfb7669fa749fc7731f378229e2dbf29a1d1337cbde04106f02236eb29d"}, + {file = "identify-1.5.5.tar.gz", hash = "sha256:7c22c384a2c9b32c5cc891d13f923f6b2653aa83e2d75d8f79be240d6c86c4f4"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -2099,8 +2147,8 @@ ipykernel = [ {file = "ipykernel-5.3.4.tar.gz", hash = "sha256:9b2652af1607986a1b231c62302d070bc0534f564c393a5d9d130db9abbbe89d"}, ] ipython = [ - {file = "ipython-7.17.0-py3-none-any.whl", hash = "sha256:5a8f159ca8b22b9a0a1f2a28befe5ad2b703339afb58c2ffe0d7c8d7a3af5999"}, - {file = "ipython-7.17.0.tar.gz", hash = "sha256:b70974aaa2674b05eb86a910c02ed09956a33f2dd6c71afc60f0b128a77e7f28"}, + {file = "ipython-7.18.1-py3-none-any.whl", hash = "sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8"}, + {file = "ipython-7.18.1.tar.gz", hash = "sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, @@ -2127,16 +2175,20 @@ jsonschema = [ {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, ] jupyter-client = [ - {file = "jupyter_client-6.1.6-py3-none-any.whl", hash = "sha256:7ad9aa91505786420d77edc5f9fb170d51050c007338ba8d196f603223fd3b3a"}, - {file = "jupyter_client-6.1.6.tar.gz", hash = "sha256:b360f8d4638bc577a4656e93f86298db755f915098dc763f6fc05da0c5d7a595"}, + {file = "jupyter_client-6.1.7-py3-none-any.whl", hash = "sha256:c958d24d6eacb975c1acebb68ac9077da61b5f5c040f22f6849928ad7393b950"}, + {file = "jupyter_client-6.1.7.tar.gz", hash = "sha256:49e390b36fe4b4226724704ea28d9fb903f1a3601b6882ce3105221cd09377a1"}, ] jupyter-core = [ {file = "jupyter_core-4.6.3-py2.py3-none-any.whl", hash = "sha256:a4ee613c060fe5697d913416fc9d553599c05e4492d58fac1192c9a6844abb21"}, {file = "jupyter_core-4.6.3.tar.gz", hash = "sha256:394fd5dd787e7c8861741880bdf8a00ce39f95de5d18e579c74b882522219e7e"}, ] jupyterlab = [ - {file = "jupyterlab-2.2.4-py3-none-any.whl", hash = "sha256:8f4cfebf81727f6bbfc59faa7ade38493a52015736f077f815488881315a6c92"}, - {file = "jupyterlab-2.2.4.tar.gz", hash = "sha256:e9d26c4c1cf4f7760dfa9ccd3fd5ea5027ae2767f22c7766dbb2fbb5e5dfcd4b"}, + {file = "jupyterlab-2.2.8-py3-none-any.whl", hash = "sha256:95d0509557881cfa8a5fcdf225f2fca46faf1bc52fc56a28e0b72fcc594c90ab"}, + {file = "jupyterlab-2.2.8.tar.gz", hash = "sha256:c8377bee30504919c1e79949f9fe35443ab7f5c4be622c95307e8108410c8b8c"}, +] +jupyterlab-pygments = [ + {file = "jupyterlab_pygments-0.1.2-py2.py3-none-any.whl", hash = "sha256:abfb880fd1561987efaefcb2d2ac75145d2a5d0139b1876d5be806e32f630008"}, + {file = "jupyterlab_pygments-0.1.2.tar.gz", hash = "sha256:cfcda0873626150932f438eccf0f8bf22bfa92345b814890ab360d666b254146"}, ] jupyterlab-server = [ {file = "jupyterlab_server-1.2.0-py3-none-any.whl", hash = "sha256:55d256077bf13e5bc9e8fbd5aac51bef82f6315111cec6b712b9a5ededbba924"}, @@ -2212,10 +2264,6 @@ mistune = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, ] -more-itertools = [ - {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, - {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, -] mypy = [ {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"}, @@ -2239,74 +2287,83 @@ mypy-extensions = [ nb-black = [ {file = "nb_black-1.0.7.tar.gz", hash = "sha256:1ca52e3a46675f6a0a6d79ac73a1f8f951bef60f919eced56173e76ab1b6d62b"}, ] +nbclient = [ + {file = "nbclient-0.5.0-py3-none-any.whl", hash = "sha256:8a6e27ff581cee50895f44c41936ce02369674e85e2ad58643d8d4a6c36771b0"}, + {file = "nbclient-0.5.0.tar.gz", hash = "sha256:8ad52d27ba144fca1402db014857e53c5a864a2f407be66ca9d74c3a56d6591d"}, +] nbconvert = [ - {file = "nbconvert-5.6.1-py2.py3-none-any.whl", hash = "sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee"}, - {file = "nbconvert-5.6.1.tar.gz", hash = "sha256:21fb48e700b43e82ba0e3142421a659d7739b65568cc832a13976a77be16b523"}, + {file = "nbconvert-6.0.6-py3-none-any.whl", hash = "sha256:d8549f62e739a4d51f275c2932b1783ee5039dde07a2b71de70c0296a42c8394"}, + {file = "nbconvert-6.0.6.tar.gz", hash = "sha256:68335477288aab8a9b9ec03002dce59b4eb1ca967116741ec218a4e78c129efd"}, ] nbformat = [ {file = "nbformat-5.0.7-py3-none-any.whl", hash = "sha256:ea55c9b817855e2dfcd3f66d74857342612a60b1f09653440f4a5845e6e3523f"}, {file = "nbformat-5.0.7.tar.gz", hash = "sha256:54d4d6354835a936bad7e8182dcd003ca3dc0cedfee5a306090e04854343b340"}, ] +nest-asyncio = [ + {file = "nest_asyncio-1.4.1-py3-none-any.whl", hash = "sha256:a4487c4f49f2d11a7bb89a512a6886b6a5045f47097f49815b2851aaa8599cf0"}, + {file = "nest_asyncio-1.4.1.tar.gz", hash = "sha256:b86c3193abda5b2eeccf8c79894bc71c680369a178f4b068514ac00720b14e01"}, +] nodeenv = [ - {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, + {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, + {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, ] notebook = [ - {file = "notebook-6.1.1-py3-none-any.whl", hash = "sha256:4cc4e44a43a83a7c2f5e85bfdbbfe1c68bed91b857741df9e593d213a6fc2d27"}, - {file = "notebook-6.1.1.tar.gz", hash = "sha256:42391d8f3b88676e774316527599e49c11f3a7e51c41035e9e44c1b58e1398d5"}, + {file = "notebook-6.1.4-py3-none-any.whl", hash = "sha256:07b6e8b8a61aa2f780fe9a97430470485bc71262bc5cae8521f1441b910d2c88"}, + {file = "notebook-6.1.4.tar.gz", hash = "sha256:687d01f963ea20360c0b904ee7a37c3d8cda553858c8d6e33fd0afd13e89de32"}, ] nox = [ - {file = "nox-2020.5.24-py3-none-any.whl", hash = "sha256:c4509621fead99473a1401870e680b0aadadce5c88440f0532863595176d64c1"}, - {file = "nox-2020.5.24.tar.gz", hash = "sha256:61a55705736a1a73efbd18d5b262a43d55a1176546e0eb28b29064cfcffe26c0"}, + {file = "nox-2020.8.22-py3-none-any.whl", hash = "sha256:55f8cab16bcfaaea08b141c83bf2b7c779e943518d0de6cd9c38cd8da95d11ea"}, + {file = "nox-2020.8.22.tar.gz", hash = "sha256:efa5adcf1134012f96bcd0a496ccebd4c9e9da53a831888a2a779462440eebcf"}, ] numpy = [ - {file = "numpy-1.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff"}, - {file = "numpy-1.19.1-cp36-cp36m-win32.whl", hash = "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624"}, - {file = "numpy-1.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983"}, - {file = "numpy-1.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954"}, - {file = "numpy-1.19.1-cp37-cp37m-win32.whl", hash = "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b"}, - {file = "numpy-1.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055"}, - {file = "numpy-1.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd"}, - {file = "numpy-1.19.1-cp38-cp38-win32.whl", hash = "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae"}, - {file = "numpy-1.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc"}, - {file = "numpy-1.19.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1"}, - {file = "numpy-1.19.1.zip", hash = "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491"}, + {file = "numpy-1.19.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b594f76771bc7fc8a044c5ba303427ee67c17a09b36e1fa32bde82f5c419d17a"}, + {file = "numpy-1.19.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e6ddbdc5113628f15de7e4911c02aed74a4ccff531842c583e5032f6e5a179bd"}, + {file = "numpy-1.19.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3733640466733441295b0d6d3dcbf8e1ffa7e897d4d82903169529fd3386919a"}, + {file = "numpy-1.19.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:4339741994c775396e1a274dba3609c69ab0f16056c1077f18979bec2a2c2e6e"}, + {file = "numpy-1.19.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c6646314291d8f5ea900a7ea9c4261f834b5b62159ba2abe3836f4fa6705526"}, + {file = "numpy-1.19.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7118f0a9f2f617f921ec7d278d981244ba83c85eea197be7c5a4f84af80a9c3c"}, + {file = "numpy-1.19.2-cp36-cp36m-win32.whl", hash = "sha256:9a3001248b9231ed73894c773142658bab914645261275f675d86c290c37f66d"}, + {file = "numpy-1.19.2-cp36-cp36m-win_amd64.whl", hash = "sha256:967c92435f0b3ba37a4257c48b8715b76741410467e2bdb1097e8391fccfae15"}, + {file = "numpy-1.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d526fa58ae4aead839161535d59ea9565863bb0b0bdb3cc63214613fb16aced4"}, + {file = "numpy-1.19.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eb25c381d168daf351147713f49c626030dcff7a393d5caa62515d415a6071d8"}, + {file = "numpy-1.19.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:62139af94728d22350a571b7c82795b9d59be77fc162414ada6c8b6a10ef5d02"}, + {file = "numpy-1.19.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0c66da1d202c52051625e55a249da35b31f65a81cb56e4c69af0dfb8fb0125bf"}, + {file = "numpy-1.19.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:2117536e968abb7357d34d754e3733b0d7113d4c9f1d921f21a3d96dec5ff716"}, + {file = "numpy-1.19.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54045b198aebf41bf6bf4088012777c1d11703bf74461d70cd350c0af2182e45"}, + {file = "numpy-1.19.2-cp37-cp37m-win32.whl", hash = "sha256:aba1d5daf1144b956bc87ffb87966791f5e9f3e1f6fab3d7f581db1f5b598f7a"}, + {file = "numpy-1.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:addaa551b298052c16885fc70408d3848d4e2e7352de4e7a1e13e691abc734c1"}, + {file = "numpy-1.19.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:58d66a6b3b55178a1f8a5fe98df26ace76260a70de694d99577ddeab7eaa9a9d"}, + {file = "numpy-1.19.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:59f3d687faea7a4f7f93bd9665e5b102f32f3fa28514f15b126f099b7997203d"}, + {file = "numpy-1.19.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cebd4f4e64cfe87f2039e4725781f6326a61f095bc77b3716502bed812b385a9"}, + {file = "numpy-1.19.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c35a01777f81e7333bcf276b605f39c872e28295441c265cd0c860f4b40148c1"}, + {file = "numpy-1.19.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d7ac33585e1f09e7345aa902c281bd777fdb792432d27fca857f39b70e5dd31c"}, + {file = "numpy-1.19.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:04c7d4ebc5ff93d9822075ddb1751ff392a4375e5885299445fcebf877f179d5"}, + {file = "numpy-1.19.2-cp38-cp38-win32.whl", hash = "sha256:51ee93e1fac3fe08ef54ff1c7f329db64d8a9c5557e6c8e908be9497ac76374b"}, + {file = "numpy-1.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:1669ec8e42f169ff715a904c9b2105b6640f3f2a4c4c2cb4920ae8b2785dac65"}, + {file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"}, + {file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"}, ] packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] pandas = [ - {file = "pandas-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:47a03bfef80d6812c91ed6fae43f04f2fa80a4e1b82b35aa4d9002e39529e0b8"}, - {file = "pandas-1.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0210f8fe19c2667a3817adb6de2c4fd92b1b78e1975ca60c0efa908e0985cbdb"}, - {file = "pandas-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:35db623487f00d9392d8af44a24516d6cb9f274afaf73cfcfe180b9c54e007d2"}, - {file = "pandas-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:4d1a806252001c5db7caecbe1a26e49a6c23421d85a700960f6ba093112f54a1"}, - {file = "pandas-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9f61cca5262840ff46ef857d4f5f65679b82188709d0e5e086a9123791f721c8"}, - {file = "pandas-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:182a5aeae319df391c3df4740bb17d5300dcd78034b17732c12e62e6dd79e4a4"}, - {file = "pandas-1.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:40ec0a7f611a3d00d3c666c4cceb9aa3f5bf9fbd81392948a93663064f527203"}, - {file = "pandas-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:16504f915f1ae424052f1e9b7cd2d01786f098fbb00fa4e0f69d42b22952d798"}, - {file = "pandas-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:fc714895b6de6803ac9f661abb316853d0cd657f5d23985222255ad76ccedc25"}, - {file = "pandas-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a15835c8409d5edc50b4af93be3377b5dd3eb53517e7f785060df1f06f6da0e2"}, - {file = "pandas-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0bc440493cf9dc5b36d5d46bbd5508f6547ba68b02a28234cd8e81fdce42744d"}, - {file = "pandas-1.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4b21d46728f8a6be537716035b445e7ef3a75dbd30bd31aa1b251323219d853e"}, - {file = "pandas-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0227e3a6e3a22c0e283a5041f1e3064d78fbde811217668bb966ed05386d8a7e"}, - {file = "pandas-1.1.0-cp38-cp38-win32.whl", hash = "sha256:ed60848caadeacecefd0b1de81b91beff23960032cded0ac1449242b506a3b3f"}, - {file = "pandas-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:60e20a4ab4d4fec253557d0fc9a4e4095c37b664f78c72af24860c8adcd07088"}, - {file = "pandas-1.1.0.tar.gz", hash = "sha256:b39508562ad0bb3f384b0db24da7d68a2608b9ddc85b1d931ccaaa92d5e45273"}, + {file = "pandas-1.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:eb0ac2fd04428f18b547716f70c699a7cc9c65a6947ed8c7e688d96eb91e3db8"}, + {file = "pandas-1.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:02ec9f5f0b7df7227931a884569ef0b6d32d76789c84bcac1a719dafd1f912e8"}, + {file = "pandas-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1edf6c254d2d138188e9987159978ee70e23362fe9197f3f100844a197f7e1e4"}, + {file = "pandas-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:b821f239514a9ce46dd1cd6c9298a03ed58d0235d414ea264aacc1b14916bbe4"}, + {file = "pandas-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ab6ea0f3116f408a8a59cd50158bfd19d2a024f4e221f14ab1bcd2da4f0c6fdf"}, + {file = "pandas-1.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:474fa53e3b2f3a543cbca81f7457bd1f44e7eb1be7171067636307e21b624e9c"}, + {file = "pandas-1.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9e135ce9929cd0f0ba24f0545936af17ba935f844d4c3a2b979354a73c9440e0"}, + {file = "pandas-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:188cdfbf8399bc144fa95040536b5ce3429d2eda6c9c8b238c987af7df9f128c"}, + {file = "pandas-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:08783a33989a6747317766b75be30a594a9764b9f145bb4bcc06e337930d9807"}, + {file = "pandas-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f7008ec22b92d771b145150978d930a28fab8da3a10131b01bbf39574acdad0b"}, + {file = "pandas-1.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59df9f0276aa4854d8bff28c5e5aeb74d9c6bb4d9f55d272b7124a7df40e47d0"}, + {file = "pandas-1.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:eeb64c5b3d4f2ea072ca8afdeb2b946cd681a863382ca79734f1b520b8d2fa26"}, + {file = "pandas-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c9235b37489168ed6b173551c816b50aa89f03c24a8549a8b4d47d8dc79bfb1e"}, + {file = "pandas-1.1.2-cp38-cp38-win32.whl", hash = "sha256:0936991228241db937e87f82ec552a33888dd04a2e0d5a2fa3c689f92fab09e0"}, + {file = "pandas-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:026d764d0b86ee53183aa4c0b90774b6146123eeada4e24946d7d24290777be1"}, + {file = "pandas-1.1.2.tar.gz", hash = "sha256:b64ffd87a2cfd31b40acd4b92cb72ea9a52a48165aec4c140e78fd69c45d1444"}, ] pandocfilters = [ {file = "pandocfilters-1.4.2.tar.gz", hash = "sha256:b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9"}, @@ -2320,8 +2377,8 @@ pathspec = [ {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, ] pbr = [ - {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, - {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, + {file = "pbr-5.5.0-py2.py3-none-any.whl", hash = "sha256:5adc0f9fc64319d8df5ca1e4e06eea674c26b80e6f00c530b18ce6a6592ead15"}, + {file = "pbr-5.5.0.tar.gz", hash = "sha256:14bfd98f51c78a3dd22a1ef45cf194ad79eee4a19e8e1a0d5c7f8e81ffe182ea"}, ] pep8-naming = [ {file = "pep8-naming-0.9.1.tar.gz", hash = "sha256:a33d38177056321a167decd6ba70b890856ba5025f0a8eca6a3eda607da93caf"}, @@ -2340,31 +2397,31 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] pre-commit = [ - {file = "pre_commit-2.6.0-py2.py3-none-any.whl", hash = "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626"}, - {file = "pre_commit-2.6.0.tar.gz", hash = "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915"}, + {file = "pre_commit-2.7.1-py2.py3-none-any.whl", hash = "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a"}, + {file = "pre_commit-2.7.1.tar.gz", hash = "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70"}, ] prometheus-client = [ {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, {file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.6-py3-none-any.whl", hash = "sha256:683397077a64cd1f750b71c05afcfc6612a7300cb6932666531e5a54f38ea564"}, - {file = "prompt_toolkit-3.0.6.tar.gz", hash = "sha256:7630ab85a23302839a0f26b31cc24f518e6155dea1ed395ea61b42c45941b6a6"}, + {file = "prompt_toolkit-3.0.7-py3-none-any.whl", hash = "sha256:83074ee28ad4ba6af190593d4d4c607ff525272a504eb159199b6dd9f950c950"}, + {file = "prompt_toolkit-3.0.7.tar.gz", hash = "sha256:822f4605f28f7d2ba6b0b09a31e25e140871e96364d1d377667b547bb3bf4489"}, ] psycopg2 = [ - {file = "psycopg2-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:a0984ff49e176062fcdc8a5a2a670c9bb1704a2f69548bce8f8a7bad41c661bf"}, - {file = "psycopg2-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:acf56d564e443e3dea152efe972b1434058244298a94348fc518d6dd6a9fb0bb"}, - {file = "psycopg2-2.8.5-cp34-cp34m-win32.whl", hash = "sha256:440a3ea2c955e89321a138eb7582aa1d22fe286c7d65e26a2c5411af0a88ae72"}, - {file = "psycopg2-2.8.5-cp34-cp34m-win_amd64.whl", hash = "sha256:6b306dae53ec7f4f67a10942cf8ac85de930ea90e9903e2df4001f69b7833f7e"}, - {file = "psycopg2-2.8.5-cp35-cp35m-win32.whl", hash = "sha256:d3b29d717d39d3580efd760a9a46a7418408acebbb784717c90d708c9ed5f055"}, - {file = "psycopg2-2.8.5-cp35-cp35m-win_amd64.whl", hash = "sha256:6a471d4d2a6f14c97a882e8d3124869bc623f3df6177eefe02994ea41fd45b52"}, - {file = "psycopg2-2.8.5-cp36-cp36m-win32.whl", hash = "sha256:27c633f2d5db0fc27b51f1b08f410715b59fa3802987aec91aeb8f562724e95c"}, - {file = "psycopg2-2.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2df2bf1b87305bd95eb3ac666ee1f00a9c83d10927b8144e8e39644218f4cf81"}, - {file = "psycopg2-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:ac5b23d0199c012ad91ed1bbb971b7666da651c6371529b1be8cbe2a7bf3c3a9"}, - {file = "psycopg2-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2c0afb40cfb4d53487ee2ebe128649028c9a78d2476d14a67781e45dc287f080"}, - {file = "psycopg2-2.8.5-cp38-cp38-win32.whl", hash = "sha256:2327bf42c1744a434ed8ed0bbaa9168cac7ee5a22a9001f6fc85c33b8a4a14b7"}, - {file = "psycopg2-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:132efc7ee46a763e68a815f4d26223d9c679953cd190f1f218187cb60decf535"}, - {file = "psycopg2-2.8.5.tar.gz", hash = "sha256:f7d46240f7a1ae1dd95aab38bd74f7428d46531f69219954266d669da60c0818"}, + {file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"}, + {file = "psycopg2-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5"}, + {file = "psycopg2-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:b8cae8b2f022efa1f011cc753adb9cbadfa5a184431d09b273fb49b4167561ad"}, + {file = "psycopg2-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:f22ea9b67aea4f4a1718300908a2fb62b3e4276cf00bd829a97ab5894af42ea3"}, + {file = "psycopg2-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:26e7fd115a6db75267b325de0fba089b911a4a12ebd3d0b5e7acb7028bc46821"}, + {file = "psycopg2-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:00195b5f6832dbf2876b8bf77f12bdce648224c89c880719c745b90515233301"}, + {file = "psycopg2-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a49833abfdede8985ba3f3ec641f771cca215479f41523e99dace96d5b8cce2a"}, + {file = "psycopg2-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:f974c96fca34ae9e4f49839ba6b78addf0346777b46c4da27a7bf54f48d3057d"}, + {file = "psycopg2-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:6a3d9efb6f36f1fe6aa8dbb5af55e067db802502c55a9defa47c5a1dad41df84"}, + {file = "psycopg2-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:56fee7f818d032f802b8eed81ef0c1232b8b42390df189cab9cfa87573fe52c5"}, + {file = "psycopg2-2.8.6-cp38-cp38-win32.whl", hash = "sha256:ad2fe8a37be669082e61fb001c185ffb58867fdbb3e7a6b0b0d2ffe232353a3e"}, + {file = "psycopg2-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:56007a226b8e95aa980ada7abdea6b40b75ce62a433bd27cec7a8178d57f4051"}, + {file = "psycopg2-2.8.6.tar.gz", hash = "sha256:fb23f6c71107c37fd667cb4ea363ddeb936b348bbd6449278eb92c189699f543"}, ] ptyprocess = [ {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, @@ -2383,35 +2440,35 @@ pycparser = [ {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] pydocstyle = [ - {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, - {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"}, + {file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"}, + {file = "pydocstyle-5.1.1.tar.gz", hash = "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325"}, ] pyflakes = [ {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pygments = [ - {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, - {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, + {file = "Pygments-2.7.1-py3-none-any.whl", hash = "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998"}, + {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"}, ] pylint = [ - {file = "pylint-2.5.3-py3-none-any.whl", hash = "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"}, - {file = "pylint-2.5.3.tar.gz", hash = "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc"}, + {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, + {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pyrsistent = [ - {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"}, + {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, ] pytest = [ - {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, - {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, + {file = "pytest-6.1.0-py3-none-any.whl", hash = "sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33"}, + {file = "pytest-6.1.0.tar.gz", hash = "sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7"}, ] pytest-cov = [ - {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, - {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, + {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, + {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, ] pytest-env = [ {file = "pytest-env-0.6.2.tar.gz", hash = "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"}, @@ -2505,27 +2562,27 @@ pyzmq = [ {file = "pyzmq-19.0.2.tar.gz", hash = "sha256:296540a065c8c21b26d63e3cea2d1d57902373b16e4256afe46422691903a438"}, ] regex = [ - {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, - {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, - {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, - {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, - {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, - {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, - {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, - {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, - {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, + {file = "regex-2020.9.27-cp27-cp27m-win32.whl", hash = "sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3"}, + {file = "regex-2020.9.27-cp27-cp27m-win_amd64.whl", hash = "sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19"}, + {file = "regex-2020.9.27-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be"}, + {file = "regex-2020.9.27-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b"}, + {file = "regex-2020.9.27-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc"}, + {file = "regex-2020.9.27-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b"}, + {file = "regex-2020.9.27-cp36-cp36m-win32.whl", hash = "sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63"}, + {file = "regex-2020.9.27-cp36-cp36m-win_amd64.whl", hash = "sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b"}, + {file = "regex-2020.9.27-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7"}, + {file = "regex-2020.9.27-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc"}, + {file = "regex-2020.9.27-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c"}, + {file = "regex-2020.9.27-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100"}, + {file = "regex-2020.9.27-cp37-cp37m-win32.whl", hash = "sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707"}, + {file = "regex-2020.9.27-cp37-cp37m-win_amd64.whl", hash = "sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab"}, + {file = "regex-2020.9.27-cp38-cp38-manylinux1_i686.whl", hash = "sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef"}, + {file = "regex-2020.9.27-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121"}, + {file = "regex-2020.9.27-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b"}, + {file = "regex-2020.9.27-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637"}, + {file = "regex-2020.9.27-cp38-cp38-win32.whl", hash = "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"}, + {file = "regex-2020.9.27-cp38-cp38-win_amd64.whl", hash = "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c"}, + {file = "regex-2020.9.27.tar.gz", hash = "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d"}, ] requests = [ {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, @@ -2551,8 +2608,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] sphinx = [ - {file = "Sphinx-3.2.0-py3-none-any.whl", hash = "sha256:f7db5b76c42c8b5ef31853c2de7178ef378b985d7793829ec071e120dac1d0ca"}, - {file = "Sphinx-3.2.0.tar.gz", hash = "sha256:cf2d5bc3c6c930ab0a1fbef3ad8a82994b1bf4ae923f8098a05c7e5516f07177"}, + {file = "Sphinx-3.2.1-py3-none-any.whl", hash = "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0"}, + {file = "Sphinx-3.2.1.tar.gz", hash = "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8"}, ] sphinx-autodoc-typehints = [ {file = "sphinx-autodoc-typehints-1.11.0.tar.gz", hash = "sha256:bbf0b203f1019b0f9843ee8eef0cff856dc04b341f6dbe1113e37f2ebf243e11"}, @@ -2583,46 +2640,50 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, ] sqlalchemy = [ - {file = "SQLAlchemy-1.3.18-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-win32.whl", hash = "sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27m-win_amd64.whl", hash = "sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1"}, - {file = "SQLAlchemy-1.3.18-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-win32.whl", hash = "sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3"}, - {file = "SQLAlchemy-1.3.18-cp35-cp35m-win_amd64.whl", hash = "sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-win32.whl", hash = "sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299"}, - {file = "SQLAlchemy-1.3.18-cp36-cp36m-win_amd64.whl", hash = "sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-win32.whl", hash = "sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e"}, - {file = "SQLAlchemy-1.3.18-cp37-cp37m-win_amd64.whl", hash = "sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-win32.whl", hash = "sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7"}, - {file = "SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl", hash = "sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8"}, - {file = "SQLAlchemy-1.3.18.tar.gz", hash = "sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7"}, + {file = "SQLAlchemy-1.3.19-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc"}, + {file = "SQLAlchemy-1.3.19-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36"}, + {file = "SQLAlchemy-1.3.19-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de"}, + {file = "SQLAlchemy-1.3.19-cp27-cp27m-win32.whl", hash = "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338"}, + {file = "SQLAlchemy-1.3.19-cp27-cp27m-win_amd64.whl", hash = "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23"}, + {file = "SQLAlchemy-1.3.19-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f"}, + {file = "SQLAlchemy-1.3.19-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc"}, + {file = "SQLAlchemy-1.3.19-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce"}, + {file = "SQLAlchemy-1.3.19-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804"}, + {file = "SQLAlchemy-1.3.19-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac"}, + {file = "SQLAlchemy-1.3.19-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12"}, + {file = "SQLAlchemy-1.3.19-cp35-cp35m-win32.whl", hash = "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b"}, + {file = "SQLAlchemy-1.3.19-cp35-cp35m-win_amd64.whl", hash = "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d"}, + {file = "SQLAlchemy-1.3.19-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea"}, + {file = "SQLAlchemy-1.3.19-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365"}, + {file = "SQLAlchemy-1.3.19-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02"}, + {file = "SQLAlchemy-1.3.19-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6"}, + {file = "SQLAlchemy-1.3.19-cp36-cp36m-win32.whl", hash = "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1"}, + {file = "SQLAlchemy-1.3.19-cp36-cp36m-win_amd64.whl", hash = "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0"}, + {file = "SQLAlchemy-1.3.19-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e"}, + {file = "SQLAlchemy-1.3.19-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe"}, + {file = "SQLAlchemy-1.3.19-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d"}, + {file = "SQLAlchemy-1.3.19-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66"}, + {file = "SQLAlchemy-1.3.19-cp37-cp37m-win32.whl", hash = "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba"}, + {file = "SQLAlchemy-1.3.19-cp37-cp37m-win_amd64.whl", hash = "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6"}, + {file = "SQLAlchemy-1.3.19-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37"}, + {file = "SQLAlchemy-1.3.19-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7"}, + {file = "SQLAlchemy-1.3.19-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb"}, + {file = "SQLAlchemy-1.3.19-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0"}, + {file = "SQLAlchemy-1.3.19-cp38-cp38-win32.whl", hash = "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86"}, + {file = "SQLAlchemy-1.3.19-cp38-cp38-win_amd64.whl", hash = "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea"}, + {file = "SQLAlchemy-1.3.19.tar.gz", hash = "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e"}, ] stevedore = [ - {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, - {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, + {file = "stevedore-3.2.2-py3-none-any.whl", hash = "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62"}, + {file = "stevedore-3.2.2.tar.gz", hash = "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"}, ] terminado = [ - {file = "terminado-0.8.3-py2.py3-none-any.whl", hash = "sha256:a43dcb3e353bc680dd0783b1d9c3fc28d529f190bc54ba9a229f72fe6e7a54d7"}, - {file = "terminado-0.8.3.tar.gz", hash = "sha256:4804a774f802306a7d9af7322193c5390f1da0abb429e082a10ef1d46e6fb2c2"}, + {file = "terminado-0.9.1-py3-none-any.whl", hash = "sha256:c55f025beb06c2e2669f7ba5a04f47bb3304c30c05842d4981d8f0fc9ab3b4e3"}, + {file = "terminado-0.9.1.tar.gz", hash = "sha256:3da72a155b807b01c9e8a5babd214e052a0a45a975751da3521a1c3381ce6d76"}, ] testfixtures = [ - {file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"}, - {file = "testfixtures-6.14.1.tar.gz", hash = "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366"}, + {file = "testfixtures-6.14.2-py2.py3-none-any.whl", hash = "sha256:816557888877f498081c1b5c572049b4a2ddffedb77401308ff4cdc1bb9147b7"}, + {file = "testfixtures-6.14.2.tar.gz", hash = "sha256:14d9907390f5f9c7189b3d511b64f34f1072d07cc13b604a57e1bb79029376e3"}, ] testpath = [ {file = "testpath-0.4.4-py2.py3-none-any.whl", hash = "sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4"}, @@ -2644,8 +2705,8 @@ tornado = [ {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] traitlets = [ - {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, - {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, + {file = "traitlets-5.0.4-py3-none-any.whl", hash = "sha256:9664ec0c526e48e7b47b7d14cd6b252efa03e0129011de0a9c1d70315d4309c3"}, + {file = "traitlets-5.0.4.tar.gz", hash = "sha256:86c9351f94f95de9db8a04ad8e892da299a088a64fd283f9f6f18770ae5eae1b"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, @@ -2671,17 +2732,17 @@ typed-ast = [ {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, - {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, - {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, + {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.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, ] virtualenv = [ - {file = "virtualenv-20.0.30-py2.py3-none-any.whl", hash = "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e"}, - {file = "virtualenv-20.0.30.tar.gz", hash = "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5"}, + {file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"}, + {file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, From 4c633cec3db3d12788fe4e6f6360c6add14c8f5d Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Wed, 30 Sep 2020 12:20:21 +0200 Subject: [PATCH 14/14] Finalize release 0.2.0 --- README.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ac69362..799030d 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ and `poetry install --extras research` The `--extras` option is necessary as the non-develop dependencies -are structured in the [pyproject.toml](https://github.com/webartifex/urban-meal-delivery/blob/develop/pyproject.toml) file +are structured in the [pyproject.toml](https://github.com/webartifex/urban-meal-delivery/blob/main/pyproject.toml) file into dependencies related to only the `urban-meal-delivery` source code package and dependencies used to run the [Jupyter](https://jupyter.org/) environment with the analyses. Contributions are welcome. Use the [issues](https://github.com/webartifex/urban-meal-delivery/issues) tab. -The project is licensed under the [MIT license](https://github.com/webartifex/urban-meal-delivery/blob/develop/LICENSE.txt). +The project is licensed under the [MIT license](https://github.com/webartifex/urban-meal-delivery/blob/main/LICENSE.txt). diff --git a/pyproject.toml b/pyproject.toml index 9b5b456..0fa45fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ target-version = ["py38"] [tool.poetry] name = "urban-meal-delivery" -version = "0.2.0.dev0" +version = "0.2.0" authors = ["Alexander Hess "] description = "Optimizing an urban meal delivery platform"