├── .flake8 ├── .github └── workflows │ ├── contribute.yml │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── poetry.lock ├── pydwt ├── __init__.py ├── app.py ├── context │ ├── connection.py │ └── datasources.py ├── core │ ├── __init__.py │ ├── containers.py │ ├── dag.py │ ├── enums.py │ ├── executors.py │ ├── project.py │ ├── schedule.py │ ├── task.py │ └── workflow.py └── sql │ ├── dataframe.py │ ├── materializations.py │ └── session.py ├── pyproject.toml └── tests ├── __init__.py ├── test_cli.py ├── test_connection.py ├── test_dags.py ├── test_dataframe.py ├── test_executors.py ├── test_project.py ├── test_schedule.py └── test_task.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203 -------------------------------------------------------------------------------- /.github/workflows/contribute.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "feature/*" 7 | - "hotfix/*" 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-18.04 12 | strategy: 13 | matrix: 14 | python-version: ['3.8', '3.9', '3.10', '3.11'] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Run image 22 | uses: abatilo/actions-poetry@v2 23 | with: 24 | poetry-version: '1.2.2' 25 | - name: Install Project with poetry 26 | run: poetry install 27 | - name: Run Project tests with poetry 28 | run: poetry run pytest --cov pydwt/ tests/ -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Python PR 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - develop 7 | jobs: 8 | pr-check: 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.9" 16 | - name: Run image 17 | uses: abatilo/actions-poetry@v2 18 | with: 19 | poetry-version: "1.2.2" 20 | - name: Install Project with poetry 21 | run: poetry install 22 | - name: reformat using black 23 | run: | 24 | poetry run black . 25 | 26 | - name: Check for changes after Black 27 | run: | 28 | if git diff --name-only | grep -qE '(\.py$)'; then 29 | echo "::set-output name=reformatted::true" 30 | else 31 | echo "::set-output name=reformatted::false" 32 | fi 33 | - name: Run Flake8 34 | run: poetry run flake8 pydwt/ 35 | 36 | - name: Check for Flake8 errors 37 | run: | 38 | if poetry run flake8 --count; then 39 | echo "::set-output name=flake8_errors::false" 40 | else 41 | echo "::set-output name=flake8_errors::true" 42 | fi -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and release package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.9" 16 | - name: Run image 17 | uses: abatilo/actions-poetry@v2 18 | with: 19 | poetry-version: "1.2.2" 20 | - name: publish package to pypi 21 | run: | 22 | poetry config pypi-token.pypi $PYPITOKEN 23 | poetry publish --build 24 | env: 25 | PYPITOKEN: ${{secrets.PYPITOKEN}} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # End of https://www.toptal.com/developers/gitignore/api/python 174 | 175 | state/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pydwt 2 | 3 | The pydwt library provides a set of tools for orchestrating tasks of data processing in a directed acyclic graph (DAG). This DAG is composed of tasks that have dependencies between them and can be executed in parallel or sequentially, depending on their dependencies. 4 | 5 | ## Installation 6 | `pip install pydwt` 7 | 8 | ## Guide 9 | In this document, we will provide a brief explanation of the main modules of the pydwt library, which are: 10 | 11 | * `session.py`: module for interacting with a database and creating DataFrame objects to manipulate data. 12 | * `dataframe.py`: module for defining a DataFrame class for working with data. 13 | * `task.py`: module for defining tasks in the DAG. 14 | * `dag.py`: module for creating and traversing the DAG. 15 | * `workflow.py`: module for running the DAG. 16 | 17 | 18 | ## Session 19 | The `session.py` module is responsible for interacting with a database and creating DataFrame objects to manipulate data. To use this module, you need to create an instance of the Session class, passing an SQLAlchemy engine object and the schema of the database (if it has one). Then, you can use the table method to create a DataFrame object from a table in the database. 20 | 21 | Here is an example of how to create a Session object and use the table method to create a DataFrame object: 22 | 23 | ```python 24 | from sqlalchemy import create_engine 25 | from pydwt.sql.session import Session 26 | 27 | engine = create_engine("postgresql://user:password@localhost/dbname") 28 | session = Session(engine, schema="my_schema") 29 | 30 | df = session.table("my_table") 31 | ``` 32 | 33 | ## DataFrame 34 | 35 | The `dataframe.py` module defines a DataFrame class for working with data. A DataFrame object is essentially a table with labeled columns and rows. You can use it to perform operations such as selecting, filtering, grouping, and aggregating data. 36 | 37 | You can also materialize a DataFrame as a table or view in the database by calling the materialize method. 38 | 39 | Here is an example of how to create a DataFrame object and perform some operations on it: 40 | 41 | ```python 42 | from pydwt.sql.session import Session 43 | from pydwt.sql.dataframe import DataFrame 44 | 45 | session = Session(engine, schema="my_schema") 46 | 47 | df = session.table("my_table") 48 | 49 | # select some columns 50 | df = df.select(df.col1, df.col2) 51 | 52 | # filter rows based on a condition 53 | df = df.where(df.col1 > 10) 54 | 55 | # group by a column and aggregate another column 56 | df = df.group_by(df.col2, agg={df.col1: (func.sum, "sum_col1")}) 57 | 58 | # show the resulting DataFrame 59 | df.materialize("new_table", as_="table") 60 | 61 | ``` 62 | ## Task 63 | 64 | The `task.py` module defines a Task class for representing a task in the DAG. A Task object has a run method that is responsible for executing the task. You can also define the task's dependencies, schedule, and other parameters when creating the object. 65 | 66 | To create a Task object, you can use the `@Task` decorator and define the run method. Here is an example of how to create a Task object: 67 | 68 | ```python 69 | from pydwt.core.task import Task 70 | 71 | @Task() 72 | def task_one(): 73 | df = session.table("features") 74 | df = df.with_column("new_column", case((df.preds == "hw", "W"))) 75 | df.materialize("new_table", as_="table") 76 | 77 | 78 | @Task(depends_on=[task_one]) 79 | def task_two(): 80 | df = session.table("new_table") 81 | df = df.where((df.new_column == "W")) 82 | df = df.with_column("new_column", case((df.preds == "hw", "W"))) 83 | df.show() 84 | 85 | ``` 86 | 87 | ## Create a new pydwt project: 88 | 89 | `pydwt new ` 90 | 91 | This command will create a new project with the name "my_project" and the required file structure. 92 | ``` 93 | my_project/ 94 | models/ 95 | example.py 96 | dags/ 97 | settings.yml 98 | ``` 99 | 100 | * `project_name/models`: where you will put your tasks 101 | * `project_name/dags/`: where the corresponding dag PNG file will be 102 | * `settings.yml`: a configuration file for your project. This file includes the configuration options for your project, such as the path to your data directory. 103 | 104 | 105 | 106 | ## Export the DAG 107 | 108 | `pydwt export-dag` 109 | 110 | will export the current state of your dag in the `project_name/dags/` as PNG file with timestamp. 111 | 112 | ## Run your project 113 | 114 | `pydwt run ` 115 | 116 | If no argument provided will run the current state of your DAG. It will process the tasks in the DAG by level and parallelize 117 | it with the `ThreadExecutor`. It a task failed then its child tasks will not be run. 118 | 119 | If argument provided in the form of `module.function_name` for instance `example.task_one` then will run all tasks in the dag leading to this task. 120 | If parent tasks succeeded then run the task. 121 | 122 | 123 | ## Test your connection setup 124 | 125 | `pydwt test-connection` 126 | 127 | will test the current setup of your DB connectiona according to your `settings.yml` file. 128 | 129 | ## Configuration of your pydwt project 130 | 131 | The `settings.yml` file is a configuration file for your pydwt project. It stores various settings such as the project name, database connection details, and DAG tasks. 132 | 133 | ### connection 134 | The connection section contains the configuration details for connecting to the database. The available options are: 135 | 136 | * `url`: the connection string to your db 137 | 138 | You can add others keys that will be forwarded to the underlying `create_engine` function 139 | for instance you can add a `echo : true` and it will call `create_engine(url=url, echo=echo)` 140 | see [here](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine) supported args. 141 | 142 | ### project 143 | The project section contains the project-related settings. The available options are: 144 | 145 | `name`: the name of the project 146 | 147 | ### tasks 148 | This section contains the configuration for each task defined in the pydwt project. 149 | 150 | Each task is identified by its name, and the configuration is stored as a dictionary. 151 | 152 | The dictionary can contain any key-value pairs that the task implementation may need to use, but it must have a key named `materialize`. 153 | 154 | * The `materialize` key specifies how the task output should be stored. The value can be either `view` or `table`. 155 | The value of the materialize key determines whether the task output should be stored as a SQL view or a SQL table. If the value is view, the output is stored as a SQL view. If the value is table, the output is stored as a SQL table. 156 | 157 | Each task implementation can access its configuration by injecting the config argument and specifying Provide[Container.config.tasks.]. The injected config argument is a dictionary containing the configuration for the specified task. 158 | 159 | example : 160 | 161 | ```python 162 | from pydwt.core.task import Task 163 | from dependency_injector.wiring import inject, Provide 164 | from pydwt.core.containers import Container 165 | 166 | @Task() 167 | @inject 168 | def task_one(config:dict = Provide[Container.config.tasks.task_one]): 169 | print(config) 170 | 171 | @Task(depends_on=[task_one]) 172 | def task_two(): 173 | print("somme processing") 174 | ``` 175 | ### sources 176 | 177 | The sources section contains the database sources that can be used in the project. Each source must have a unique name and specify the schema and table to use for the source. 178 | 179 | ```yaml 180 | sources: 181 | name_alias: 182 | table: xxx 183 | schema: yyy 184 | ``` 185 | 186 | You can then required this datasource in your tasks 187 | 188 | ```python 189 | from pydwt.core.task import Task 190 | from dependency_injector.wiring import inject, Provide 191 | from pydwt.core.containers import Container 192 | 193 | @Task() 194 | @inject 195 | def task_one( 196 | config:dict = Provide[Container.config.tasks.task_one], 197 | repo= Provide[Container.datasources], 198 | ): 199 | df = repo.get_sources("name_alias") 200 | ``` 201 | 202 | 203 | 204 | ## License 205 | This project is licensed under GPL. -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "attrs" 3 | version = "22.2.0" 4 | description = "Classes Without Boilerplate" 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.extras] 10 | cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 11 | dev = ["attrs[docs,tests]"] 12 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] 13 | tests = ["attrs[tests-no-zope]", "zope.interface"] 14 | tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] 15 | 16 | [[package]] 17 | name = "black" 18 | version = "23.1.0" 19 | description = "The uncompromising code formatter." 20 | category = "dev" 21 | optional = false 22 | python-versions = ">=3.7" 23 | 24 | [package.dependencies] 25 | click = ">=8.0.0" 26 | mypy-extensions = ">=0.4.3" 27 | packaging = ">=22.0" 28 | pathspec = ">=0.9.0" 29 | platformdirs = ">=2" 30 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 31 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 32 | 33 | [package.extras] 34 | colorama = ["colorama (>=0.4.3)"] 35 | d = ["aiohttp (>=3.7.4)"] 36 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 37 | uvloop = ["uvloop (>=0.15.2)"] 38 | 39 | [[package]] 40 | name = "click" 41 | version = "8.1.3" 42 | description = "Composable command line interface toolkit" 43 | category = "main" 44 | optional = false 45 | python-versions = ">=3.7" 46 | 47 | [package.dependencies] 48 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 49 | 50 | [[package]] 51 | name = "colorama" 52 | version = "0.4.6" 53 | description = "Cross-platform colored terminal text." 54 | category = "main" 55 | optional = false 56 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 57 | 58 | [[package]] 59 | name = "contourpy" 60 | version = "1.0.7" 61 | description = "Python library for calculating contours of 2D quadrilateral grids" 62 | category = "main" 63 | optional = false 64 | python-versions = ">=3.8" 65 | 66 | [package.dependencies] 67 | numpy = ">=1.16" 68 | 69 | [package.extras] 70 | bokeh = ["bokeh", "chromedriver", "selenium"] 71 | docs = ["furo", "sphinx-copybutton"] 72 | mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"] 73 | test = ["Pillow", "matplotlib", "pytest"] 74 | test-no-images = ["pytest"] 75 | 76 | [[package]] 77 | name = "coverage" 78 | version = "7.2.1" 79 | description = "Code coverage measurement for Python" 80 | category = "dev" 81 | optional = false 82 | python-versions = ">=3.7" 83 | 84 | [package.dependencies] 85 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 86 | 87 | [package.extras] 88 | toml = ["tomli"] 89 | 90 | [[package]] 91 | name = "cycler" 92 | version = "0.11.0" 93 | description = "Composable style cycles" 94 | category = "main" 95 | optional = false 96 | python-versions = ">=3.6" 97 | 98 | [[package]] 99 | name = "dependency-injector" 100 | version = "4.41.0" 101 | description = "Dependency injection framework for Python" 102 | category = "main" 103 | optional = false 104 | python-versions = "*" 105 | 106 | [package.dependencies] 107 | six = ">=1.7.0,<=1.16.0" 108 | 109 | [package.extras] 110 | aiohttp = ["aiohttp"] 111 | flask = ["flask"] 112 | pydantic = ["pydantic"] 113 | yaml = ["pyyaml"] 114 | 115 | [[package]] 116 | name = "exceptiongroup" 117 | version = "1.1.0" 118 | description = "Backport of PEP 654 (exception groups)" 119 | category = "dev" 120 | optional = false 121 | python-versions = ">=3.7" 122 | 123 | [package.extras] 124 | test = ["pytest (>=6)"] 125 | 126 | [[package]] 127 | name = "flake8" 128 | version = "6.0.0" 129 | description = "the modular source code checker: pep8 pyflakes and co" 130 | category = "dev" 131 | optional = false 132 | python-versions = ">=3.8.1" 133 | 134 | [package.dependencies] 135 | mccabe = ">=0.7.0,<0.8.0" 136 | pycodestyle = ">=2.10.0,<2.11.0" 137 | pyflakes = ">=3.0.0,<3.1.0" 138 | 139 | [[package]] 140 | name = "fonttools" 141 | version = "4.38.0" 142 | description = "Tools to manipulate font files" 143 | category = "main" 144 | optional = false 145 | python-versions = ">=3.7" 146 | 147 | [package.extras] 148 | all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=14.0.0)", "xattr", "zopfli (>=0.1.4)"] 149 | graphite = ["lz4 (>=1.7.4.2)"] 150 | interpolatable = ["munkres", "scipy"] 151 | lxml = ["lxml (>=4.0,<5)"] 152 | pathops = ["skia-pathops (>=0.5.0)"] 153 | plot = ["matplotlib"] 154 | repacker = ["uharfbuzz (>=0.23.0)"] 155 | symfont = ["sympy"] 156 | type1 = ["xattr"] 157 | ufo = ["fs (>=2.2.0,<3)"] 158 | unicode = ["unicodedata2 (>=14.0.0)"] 159 | woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] 160 | 161 | [[package]] 162 | name = "greenlet" 163 | version = "2.0.2" 164 | description = "Lightweight in-process concurrent programming" 165 | category = "main" 166 | optional = false 167 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 168 | 169 | [package.extras] 170 | docs = ["Sphinx", "docutils (<0.18)"] 171 | test = ["objgraph", "psutil"] 172 | 173 | [[package]] 174 | name = "importlib-resources" 175 | version = "5.12.0" 176 | description = "Read resources from Python packages" 177 | category = "main" 178 | optional = false 179 | python-versions = ">=3.7" 180 | 181 | [package.dependencies] 182 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 183 | 184 | [package.extras] 185 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 186 | testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 187 | 188 | [[package]] 189 | name = "iniconfig" 190 | version = "2.0.0" 191 | description = "brain-dead simple config-ini parsing" 192 | category = "dev" 193 | optional = false 194 | python-versions = ">=3.7" 195 | 196 | [[package]] 197 | name = "kiwisolver" 198 | version = "1.4.4" 199 | description = "A fast implementation of the Cassowary constraint solver" 200 | category = "main" 201 | optional = false 202 | python-versions = ">=3.7" 203 | 204 | [[package]] 205 | name = "matplotlib" 206 | version = "3.7.0" 207 | description = "Python plotting package" 208 | category = "main" 209 | optional = false 210 | python-versions = ">=3.8" 211 | 212 | [package.dependencies] 213 | contourpy = ">=1.0.1" 214 | cycler = ">=0.10" 215 | fonttools = ">=4.22.0" 216 | importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} 217 | kiwisolver = ">=1.0.1" 218 | numpy = ">=1.20" 219 | packaging = ">=20.0" 220 | pillow = ">=6.2.0" 221 | pyparsing = ">=2.3.1" 222 | python-dateutil = ">=2.7" 223 | setuptools_scm = ">=7" 224 | 225 | [[package]] 226 | name = "mccabe" 227 | version = "0.7.0" 228 | description = "McCabe checker, plugin for flake8" 229 | category = "dev" 230 | optional = false 231 | python-versions = ">=3.6" 232 | 233 | [[package]] 234 | name = "mypy-extensions" 235 | version = "1.0.0" 236 | description = "Type system extensions for programs checked with the mypy type checker." 237 | category = "dev" 238 | optional = false 239 | python-versions = ">=3.5" 240 | 241 | [[package]] 242 | name = "networkx" 243 | version = "3.0" 244 | description = "Python package for creating and manipulating graphs and networks" 245 | category = "main" 246 | optional = false 247 | python-versions = ">=3.8" 248 | 249 | [package.extras] 250 | default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] 251 | developer = ["mypy (>=0.991)", "pre-commit (>=2.20)"] 252 | doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.2)", "pydata-sphinx-theme (>=0.11)", "sphinx (==5.2.3)", "sphinx-gallery (>=0.11)", "texext (>=0.6.7)"] 253 | extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] 254 | test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] 255 | 256 | [[package]] 257 | name = "numpy" 258 | version = "1.24.2" 259 | description = "Fundamental package for array computing in Python" 260 | category = "main" 261 | optional = false 262 | python-versions = ">=3.8" 263 | 264 | [[package]] 265 | name = "packaging" 266 | version = "23.0" 267 | description = "Core utilities for Python packages" 268 | category = "main" 269 | optional = false 270 | python-versions = ">=3.7" 271 | 272 | [[package]] 273 | name = "pathspec" 274 | version = "0.11.0" 275 | description = "Utility library for gitignore style pattern matching of file paths." 276 | category = "dev" 277 | optional = false 278 | python-versions = ">=3.7" 279 | 280 | [[package]] 281 | name = "pillow" 282 | version = "9.4.0" 283 | description = "Python Imaging Library (Fork)" 284 | category = "main" 285 | optional = false 286 | python-versions = ">=3.7" 287 | 288 | [package.extras] 289 | docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] 290 | tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] 291 | 292 | [[package]] 293 | name = "platformdirs" 294 | version = "3.0.0" 295 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 296 | category = "dev" 297 | optional = false 298 | python-versions = ">=3.7" 299 | 300 | [package.extras] 301 | docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] 302 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 303 | 304 | [[package]] 305 | name = "pluggy" 306 | version = "1.0.0" 307 | description = "plugin and hook calling mechanisms for python" 308 | category = "dev" 309 | optional = false 310 | python-versions = ">=3.6" 311 | 312 | [package.extras] 313 | dev = ["pre-commit", "tox"] 314 | testing = ["pytest", "pytest-benchmark"] 315 | 316 | [[package]] 317 | name = "pycodestyle" 318 | version = "2.10.0" 319 | description = "Python style guide checker" 320 | category = "dev" 321 | optional = false 322 | python-versions = ">=3.6" 323 | 324 | [[package]] 325 | name = "pyflakes" 326 | version = "3.0.1" 327 | description = "passive checker of Python programs" 328 | category = "dev" 329 | optional = false 330 | python-versions = ">=3.6" 331 | 332 | [[package]] 333 | name = "pyparsing" 334 | version = "3.0.9" 335 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 336 | category = "main" 337 | optional = false 338 | python-versions = ">=3.6.8" 339 | 340 | [package.extras] 341 | diagrams = ["jinja2", "railroad-diagrams"] 342 | 343 | [[package]] 344 | name = "pytest" 345 | version = "7.2.1" 346 | description = "pytest: simple powerful testing with Python" 347 | category = "dev" 348 | optional = false 349 | python-versions = ">=3.7" 350 | 351 | [package.dependencies] 352 | attrs = ">=19.2.0" 353 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 354 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 355 | iniconfig = "*" 356 | packaging = "*" 357 | pluggy = ">=0.12,<2.0" 358 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 359 | 360 | [package.extras] 361 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 362 | 363 | [[package]] 364 | name = "pytest-cov" 365 | version = "4.0.0" 366 | description = "Pytest plugin for measuring coverage." 367 | category = "dev" 368 | optional = false 369 | python-versions = ">=3.6" 370 | 371 | [package.dependencies] 372 | coverage = {version = ">=5.2.1", extras = ["toml"]} 373 | pytest = ">=4.6" 374 | 375 | [package.extras] 376 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 377 | 378 | [[package]] 379 | name = "python-dateutil" 380 | version = "2.8.2" 381 | description = "Extensions to the standard Python datetime module" 382 | category = "main" 383 | optional = false 384 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 385 | 386 | [package.dependencies] 387 | six = ">=1.5" 388 | 389 | [[package]] 390 | name = "pyyaml" 391 | version = "6.0" 392 | description = "YAML parser and emitter for Python" 393 | category = "main" 394 | optional = false 395 | python-versions = ">=3.6" 396 | 397 | [[package]] 398 | name = "setuptools" 399 | version = "67.4.0" 400 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 401 | category = "main" 402 | optional = false 403 | python-versions = ">=3.7" 404 | 405 | [package.extras] 406 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 407 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 408 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 409 | 410 | [[package]] 411 | name = "setuptools-scm" 412 | version = "7.1.0" 413 | description = "the blessed package to manage your versions by scm tags" 414 | category = "main" 415 | optional = false 416 | python-versions = ">=3.7" 417 | 418 | [package.dependencies] 419 | packaging = ">=20.0" 420 | setuptools = "*" 421 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 422 | typing-extensions = "*" 423 | 424 | [package.extras] 425 | test = ["pytest (>=6.2)", "virtualenv (>20)"] 426 | toml = ["setuptools (>=42)"] 427 | 428 | [[package]] 429 | name = "six" 430 | version = "1.16.0" 431 | description = "Python 2 and 3 compatibility utilities" 432 | category = "main" 433 | optional = false 434 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 435 | 436 | [[package]] 437 | name = "sqlalchemy" 438 | version = "2.0.4" 439 | description = "Database Abstraction Library" 440 | category = "main" 441 | optional = false 442 | python-versions = ">=3.7" 443 | 444 | [package.dependencies] 445 | greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} 446 | typing-extensions = ">=4.2.0" 447 | 448 | [package.extras] 449 | aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] 450 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] 451 | asyncio = ["greenlet (!=0.4.17)"] 452 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] 453 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] 454 | mssql = ["pyodbc"] 455 | mssql-pymssql = ["pymssql"] 456 | mssql-pyodbc = ["pyodbc"] 457 | mypy = ["mypy (>=0.910)"] 458 | mysql = ["mysqlclient (>=1.4.0)"] 459 | mysql-connector = ["mysql-connector-python"] 460 | oracle = ["cx-oracle (>=7)"] 461 | oracle-oracledb = ["oracledb (>=1.0.1)"] 462 | postgresql = ["psycopg2 (>=2.7)"] 463 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 464 | postgresql-pg8000 = ["pg8000 (>=1.29.1)"] 465 | postgresql-psycopg = ["psycopg (>=3.0.7)"] 466 | postgresql-psycopg2binary = ["psycopg2-binary"] 467 | postgresql-psycopg2cffi = ["psycopg2cffi"] 468 | pymysql = ["pymysql"] 469 | sqlcipher = ["sqlcipher3-binary"] 470 | 471 | [[package]] 472 | name = "tomli" 473 | version = "2.0.1" 474 | description = "A lil' TOML parser" 475 | category = "main" 476 | optional = false 477 | python-versions = ">=3.7" 478 | 479 | [[package]] 480 | name = "typer" 481 | version = "0.7.0" 482 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 483 | category = "main" 484 | optional = false 485 | python-versions = ">=3.6" 486 | 487 | [package.dependencies] 488 | click = ">=7.1.1,<9.0.0" 489 | 490 | [package.extras] 491 | all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 492 | dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] 493 | doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] 494 | test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] 495 | 496 | [[package]] 497 | name = "typing-extensions" 498 | version = "4.5.0" 499 | description = "Backported and Experimental Type Hints for Python 3.7+" 500 | category = "main" 501 | optional = false 502 | python-versions = ">=3.7" 503 | 504 | [[package]] 505 | name = "zipp" 506 | version = "3.15.0" 507 | description = "Backport of pathlib-compatible object wrapper for zip files" 508 | category = "main" 509 | optional = false 510 | python-versions = ">=3.7" 511 | 512 | [package.extras] 513 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 514 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 515 | 516 | [metadata] 517 | lock-version = "1.1" 518 | python-versions = "^3.8.1" 519 | content-hash = "7d510fe93f9101e492172510c167249cbb2e15b6641024f3bee4f1d6523d73d3" 520 | 521 | [metadata.files] 522 | attrs = [ 523 | {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, 524 | {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, 525 | ] 526 | black = [ 527 | {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, 528 | {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, 529 | {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, 530 | {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, 531 | {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, 532 | {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, 533 | {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, 534 | {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, 535 | {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, 536 | {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, 537 | {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, 538 | {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, 539 | {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, 540 | {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, 541 | {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, 542 | {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, 543 | {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, 544 | {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, 545 | {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, 546 | {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, 547 | {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, 548 | {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, 549 | {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, 550 | {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, 551 | {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, 552 | ] 553 | click = [ 554 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 555 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 556 | ] 557 | colorama = [ 558 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 559 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 560 | ] 561 | contourpy = [ 562 | {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"}, 563 | {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"}, 564 | {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"}, 565 | {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"}, 566 | {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"}, 567 | {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"}, 568 | {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"}, 569 | {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"}, 570 | {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"}, 571 | {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"}, 572 | {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"}, 573 | {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71"}, 574 | {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5"}, 575 | {file = "contourpy-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414"}, 576 | {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2"}, 577 | {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02"}, 578 | {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae"}, 579 | {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc"}, 580 | {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac"}, 581 | {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566"}, 582 | {file = "contourpy-1.0.7-cp311-cp311-win32.whl", hash = "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0"}, 583 | {file = "contourpy-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350"}, 584 | {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"}, 585 | {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"}, 586 | {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"}, 587 | {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"}, 588 | {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"}, 589 | {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"}, 590 | {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"}, 591 | {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"}, 592 | {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"}, 593 | {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"}, 594 | {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"}, 595 | {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"}, 596 | {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"}, 597 | {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"}, 598 | {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"}, 599 | {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"}, 600 | {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"}, 601 | {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"}, 602 | {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"}, 603 | {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"}, 604 | {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"}, 605 | {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"}, 606 | {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"}, 607 | {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"}, 608 | {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"}, 609 | {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"}, 610 | {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"}, 611 | {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"}, 612 | {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"}, 613 | {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"}, 614 | {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"}, 615 | {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"}, 616 | {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"}, 617 | ] 618 | coverage = [ 619 | {file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"}, 620 | {file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"}, 621 | {file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"}, 622 | {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"}, 623 | {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"}, 624 | {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"}, 625 | {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"}, 626 | {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"}, 627 | {file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"}, 628 | {file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"}, 629 | {file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"}, 630 | {file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"}, 631 | {file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"}, 632 | {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"}, 633 | {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"}, 634 | {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"}, 635 | {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"}, 636 | {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"}, 637 | {file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"}, 638 | {file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"}, 639 | {file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"}, 640 | {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"}, 641 | {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"}, 642 | {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"}, 643 | {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"}, 644 | {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"}, 645 | {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"}, 646 | {file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"}, 647 | {file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"}, 648 | {file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"}, 649 | {file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"}, 650 | {file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"}, 651 | {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"}, 652 | {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"}, 653 | {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"}, 654 | {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"}, 655 | {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"}, 656 | {file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"}, 657 | {file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"}, 658 | {file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"}, 659 | {file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"}, 660 | {file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"}, 661 | {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"}, 662 | {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"}, 663 | {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"}, 664 | {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"}, 665 | {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"}, 666 | {file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"}, 667 | {file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"}, 668 | {file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"}, 669 | {file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"}, 670 | ] 671 | cycler = [ 672 | {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, 673 | {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, 674 | ] 675 | dependency-injector = [ 676 | {file = "dependency-injector-4.41.0.tar.gz", hash = "sha256:939dfc657104bc3e66b67afd3fb2ebb0850c9a1e73d0d26066f2bbdd8735ff9c"}, 677 | {file = "dependency_injector-4.41.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2381a251b04244125148298212550750e6e1403e9b2850cc62e0e829d050ad3"}, 678 | {file = "dependency_injector-4.41.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75280dfa23f7c88e1bf56c3920d58a43516816de6f6ab2a6650bb8a0f27d5c2c"}, 679 | {file = "dependency_injector-4.41.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63bfba21f8bff654a80e9b9d06dd6c43a442990b73bf89cd471314c11c541ec2"}, 680 | {file = "dependency_injector-4.41.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3535d06416251715b45f8412482b58ec1c6196a4a3baa207f947f0b03a7c4b44"}, 681 | {file = "dependency_injector-4.41.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d09c08c944a25dabfb454238c1a889acd85102b93ae497de523bf9ab7947b28a"}, 682 | {file = "dependency_injector-4.41.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:586a0821720b15932addbefb00f7370fbcd5831d6ebbd6494d774b44ff96d23a"}, 683 | {file = "dependency_injector-4.41.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7fa4970f12a3fc95d8796938b11c41276ad1ff4c447b0e589212eab3fc527a90"}, 684 | {file = "dependency_injector-4.41.0-cp310-cp310-win32.whl", hash = "sha256:d557e40673de984f78dab13ebd68d27fbb2f16d7c4e3b663ea2fa2f9fae6765b"}, 685 | {file = "dependency_injector-4.41.0-cp310-cp310-win_amd64.whl", hash = "sha256:3744c327d18408e74781bd6d8b7738745ee80ef89f2c8daecf9ebd098cb84972"}, 686 | {file = "dependency_injector-4.41.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:89c67edffe7007cf33cee79ecbca38f48efcc2add5c280717af434db6c789377"}, 687 | {file = "dependency_injector-4.41.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786f7aac592e191c9caafc47732161d807bad65c62f260cd84cd73c7e2d67d6d"}, 688 | {file = "dependency_injector-4.41.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b61a15bc46a3aa7b29bd8a7384b650aa3a7ef943491e93c49a0540a0b3dda4"}, 689 | {file = "dependency_injector-4.41.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4f113e5d4c3070973ad76e5bda7317e500abae6083d78689f0b6e37cf403abf"}, 690 | {file = "dependency_injector-4.41.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fa3ed8f0700e47a0e7363f949b4525ffa8277aa1c5b10ca5b41fce4dea61bb9"}, 691 | {file = "dependency_injector-4.41.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e15ea0f2b14c1127e8b0d1597fef13f98845679f63bf670ba12dbfc12a16ef"}, 692 | {file = "dependency_injector-4.41.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3055b3fc47a0d6e5f27defb4166c0d37543a4967c279549b154afaf506ce6efc"}, 693 | {file = "dependency_injector-4.41.0-cp311-cp311-win32.whl", hash = "sha256:37d5954026e3831663518d78bdf4be9c2dbfea691edcb73c813aa3093aa4363a"}, 694 | {file = "dependency_injector-4.41.0-cp311-cp311-win_amd64.whl", hash = "sha256:f89a507e389b7e4d4892dd9a6f5f4da25849e24f73275478634ac594d621ab3f"}, 695 | {file = "dependency_injector-4.41.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ac79f3c05747f9724bd56c06985e78331fc6c85eb50f3e3f1a35e0c60f9977e9"}, 696 | {file = "dependency_injector-4.41.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75e7a733b372db3144a34020c4233f6b94db2c6342d6d16bc5245b1b941ee2bd"}, 697 | {file = "dependency_injector-4.41.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40936d9384363331910abd59dd244158ec3572abf9d37322f15095315ac99893"}, 698 | {file = "dependency_injector-4.41.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a31d9d60be4b585585081109480cfb2ef564d3b851cb32a139bf8408411a93a"}, 699 | {file = "dependency_injector-4.41.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:953bfac819d32dc72b963767589e0ed372e5e9e78b03fb6b89419d0500d34bbe"}, 700 | {file = "dependency_injector-4.41.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8f0090ff14038f17a026ca408a3a0b0e7affb6aa7498b2b59d670f40ac970fbe"}, 701 | {file = "dependency_injector-4.41.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6b29abac56ce347d2eb58a560723e1663ee2125cf5cc38866ed92b84319927ec"}, 702 | {file = "dependency_injector-4.41.0-cp36-cp36m-win32.whl", hash = "sha256:059fbb48333148143e8667a5323d162628dfe27c386bd0ed3deeecfc390338bf"}, 703 | {file = "dependency_injector-4.41.0-cp36-cp36m-win_amd64.whl", hash = "sha256:16de2797dcfcc2263b8672bf0751166f7c7b369ca2ff9246ceb67b65f8e1d802"}, 704 | {file = "dependency_injector-4.41.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c71d30b6708438050675f338edb9a25bea6c258478dbe5ec8405286756a2d347"}, 705 | {file = "dependency_injector-4.41.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d283aee588a72072439e6721cb64aa6cba5bc18c576ef0ab28285a6ec7a9d655"}, 706 | {file = "dependency_injector-4.41.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc852da612c7e347f2fcf921df2eca2718697a49f648a28a63db3ab504fd9510"}, 707 | {file = "dependency_injector-4.41.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02620454ee8101f77a317f3229935ce687480883d72a40858ff4b0c87c935cce"}, 708 | {file = "dependency_injector-4.41.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7a92680bea1c260e5c0d2d6cd60b0c913cba76a456a147db5ac047ecfcfcc758"}, 709 | {file = "dependency_injector-4.41.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:168334cba3f1cbf55299ef38f0f2e31879115cc767b780c859f7814a52d80abb"}, 710 | {file = "dependency_injector-4.41.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:48b6886a87b4ceb9b9f78550f77b2a5c7d2ce33bc83efd886556ad468cc9c85a"}, 711 | {file = "dependency_injector-4.41.0-cp37-cp37m-win32.whl", hash = "sha256:87be84084a1b922c4ba15e2e5aa900ee24b78a5467997cb7aec0a1d6cdb4a00b"}, 712 | {file = "dependency_injector-4.41.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8b8cf1c6c56f5c18bdbd9f5e93b52ca29cb4d99606d4056e91f0c761eef496dc"}, 713 | {file = "dependency_injector-4.41.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a8686fa330c83251c75c8238697686f7a0e0f6d40658538089165dc72df9bcff"}, 714 | {file = "dependency_injector-4.41.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d670a844268dcd758195e58e9a5b39fc74bb8648aba99a13135a4a10ec9cfac"}, 715 | {file = "dependency_injector-4.41.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3b9d41e0eff4c8e16fea1e33de66ff0030fe51137ca530f3c52ce110447914"}, 716 | {file = "dependency_injector-4.41.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a724e0a737baadb4378f5dc1b079867cc3a88552fcca719b3dba84716828b2"}, 717 | {file = "dependency_injector-4.41.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3588bd887b051d16b8bcabaae1127eb14059a0719a8fe34c8a75ba59321b352c"}, 718 | {file = "dependency_injector-4.41.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:409441122f40e1b4b8582845fdd76deb9dc5c9d6eb74a057b85736ef9e9c671f"}, 719 | {file = "dependency_injector-4.41.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7dcba8665cafec825b7095d5dd80afb5cf14404450eca3fe8b66e1edbf4dbc10"}, 720 | {file = "dependency_injector-4.41.0-cp38-cp38-win32.whl", hash = "sha256:8b51efeaebacaf79ef68edfc65e9687699ccffb3538c4a3ab30d0d77e2db7189"}, 721 | {file = "dependency_injector-4.41.0-cp38-cp38-win_amd64.whl", hash = "sha256:1662e2ef60ac6e681b9e11b5d8b7c17a0f733688916cf695f9540f8f50a61b1e"}, 722 | {file = "dependency_injector-4.41.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51217cb384b468d7cc355544cec20774859f00812f9a1a71ed7fa701c957b2a7"}, 723 | {file = "dependency_injector-4.41.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3890a12423ae3a9eade035093beba487f8d092ee6c6cb8706f4e7080a56e819"}, 724 | {file = "dependency_injector-4.41.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99ed73b1521bf249e2823a08a730c9f9413a58f4b4290da022e0ad4fb333ba3d"}, 725 | {file = "dependency_injector-4.41.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300838e9d4f3fbf539892a5a4072851728e23b37a1f467afcf393edd994d88f0"}, 726 | {file = "dependency_injector-4.41.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56d37b9d2f50a18f059d9abdbea7669a7518bd42b81603c21a27910a2b3f1657"}, 727 | {file = "dependency_injector-4.41.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4a44ca3ce5867513a70b31855b218be3d251f5068ce1c480cc3a4ad24ffd3280"}, 728 | {file = "dependency_injector-4.41.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67b369592c57549ccdcad0d5fef1ddb9d39af7fed8083d76e789ab0111fc6389"}, 729 | {file = "dependency_injector-4.41.0-cp39-cp39-win32.whl", hash = "sha256:740a8e8106a04d3f44b52b25b80570fdac96a8a3934423de7c9202c5623e7936"}, 730 | {file = "dependency_injector-4.41.0-cp39-cp39-win_amd64.whl", hash = "sha256:22b11dbf696e184f0b3d5ac4e5418aeac3c379ba4ea758c04a83869b7e5d1cbf"}, 731 | {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b365a8548e9a49049fa6acb24d3cd939f619eeb8e300ca3e156e44402dcc07ec"}, 732 | {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5168dc59808317dc4cdd235aa5d7d556d33e5600156acaf224cead236b48a3e8"}, 733 | {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3229d83e99e255451605d5276604386e06ad948e3d60f31ddd796781c77f76f"}, 734 | {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1baee908f21190bdc46a65ce4c417a5175e9397ca62354928694fce218f84487"}, 735 | {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b37f36ecb0c1227f697e1d4a029644e3eda8dd0f0716aa63ad04d96dbb15bbbb"}, 736 | {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b0c9c966ff66c77364a2d43d08de9968aff7e3903938fe912ba49796b2133344"}, 737 | {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12e91ac0333e7e589421943ff6c6bf9cf0d9ac9703301cec37ccff3723406332"}, 738 | {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2440b32474d4e747209528ca3ae48f42563b2fbe3d74dbfe949c11dfbfef7c4"}, 739 | {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54032d62610cf2f4421c9d92cef52957215aaa0bca403cda580c58eb3f726eda"}, 740 | {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:76b94c8310929e54136f3cb3de3adc86d1a657b3984299f40bf1cd2ba0bae548"}, 741 | {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6ee9810841c6e0599356cb884d16453bfca6ab739d0e4f0248724ed8f9ee0d79"}, 742 | {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b98945edae88e777091bf0848f869fb94bd76dfa4066d7c870a5caa933391d0"}, 743 | {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2dee5d4abdd21f1a30a51d46645c095be9dcc404c7c6e9f81d0a01415a49e64"}, 744 | {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d03f5fa0fa98a18bd0dfce846db80e2798607f0b861f1f99c97f441f7669d7a2"}, 745 | {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f2842e15bae664a9f69932e922b02afa055c91efec959cb1896f6c499bf68180"}, 746 | ] 747 | exceptiongroup = [ 748 | {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, 749 | {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, 750 | ] 751 | flake8 = [ 752 | {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, 753 | {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, 754 | ] 755 | fonttools = [ 756 | {file = "fonttools-4.38.0-py3-none-any.whl", hash = "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb"}, 757 | {file = "fonttools-4.38.0.zip", hash = "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1"}, 758 | ] 759 | greenlet = [ 760 | {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, 761 | {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, 762 | {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, 763 | {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, 764 | {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, 765 | {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, 766 | {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, 767 | {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, 768 | {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, 769 | {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, 770 | {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, 771 | {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, 772 | {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, 773 | {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, 774 | {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, 775 | {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, 776 | {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, 777 | {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, 778 | {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, 779 | {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, 780 | {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, 781 | {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, 782 | {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, 783 | {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, 784 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, 785 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, 786 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, 787 | {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, 788 | {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, 789 | {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, 790 | {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, 791 | {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, 792 | {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, 793 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, 794 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, 795 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, 796 | {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, 797 | {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, 798 | {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, 799 | {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, 800 | {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, 801 | {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, 802 | {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, 803 | {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, 804 | {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, 805 | {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, 806 | {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, 807 | {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, 808 | {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, 809 | {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, 810 | {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, 811 | {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, 812 | {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, 813 | {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, 814 | {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, 815 | {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, 816 | {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, 817 | {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, 818 | {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, 819 | {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, 820 | ] 821 | importlib-resources = [ 822 | {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, 823 | {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, 824 | ] 825 | iniconfig = [ 826 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 827 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 828 | ] 829 | kiwisolver = [ 830 | {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, 831 | {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, 832 | {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, 833 | {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, 834 | {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, 835 | {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, 836 | {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, 837 | {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, 838 | {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, 839 | {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, 840 | {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, 841 | {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, 842 | {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, 843 | {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, 844 | {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, 845 | {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, 846 | {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, 847 | {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, 848 | {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, 849 | {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, 850 | {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, 851 | {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, 852 | {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, 853 | {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, 854 | {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, 855 | {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, 856 | {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, 857 | {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, 858 | {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, 859 | {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, 860 | {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, 861 | {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, 862 | {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, 863 | {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, 864 | {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, 865 | {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, 866 | {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, 867 | {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, 868 | {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, 869 | {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, 870 | {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, 871 | {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, 872 | {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, 873 | {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, 874 | {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, 875 | {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, 876 | {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, 877 | {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, 878 | {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, 879 | {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, 880 | {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, 881 | {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, 882 | {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, 883 | {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, 884 | {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, 885 | {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, 886 | {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, 887 | {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, 888 | {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, 889 | {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, 890 | {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, 891 | {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, 892 | {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, 893 | {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, 894 | {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, 895 | {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, 896 | {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, 897 | {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, 898 | ] 899 | matplotlib = [ 900 | {file = "matplotlib-3.7.0-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:3da8b9618188346239e51f1ea6c0f8f05c6e218cfcc30b399dd7dd7f52e8bceb"}, 901 | {file = "matplotlib-3.7.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c0592ba57217c22987b7322df10f75ef95bc44dce781692b4b7524085de66019"}, 902 | {file = "matplotlib-3.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:21269450243d6928da81a9bed201f0909432a74e7d0d65db5545b9fa8a0d0223"}, 903 | {file = "matplotlib-3.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb2e76cd429058d8954121c334dddfcd11a6186c6975bca61f3f248c99031b05"}, 904 | {file = "matplotlib-3.7.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de20eb1247725a2f889173d391a6d9e7e0f2540feda24030748283108b0478ec"}, 905 | {file = "matplotlib-3.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5465735eaaafd1cfaec3fed60aee776aeb3fd3992aa2e49f4635339c931d443"}, 906 | {file = "matplotlib-3.7.0-cp310-cp310-win32.whl", hash = "sha256:092e6abc80cdf8a95f7d1813e16c0e99ceda8d5b195a3ab859c680f3487b80a2"}, 907 | {file = "matplotlib-3.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:4f640534ec2760e270801056bc0d8a10777c48b30966eef78a7c35d8590915ba"}, 908 | {file = "matplotlib-3.7.0-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:f336e7014889c38c59029ebacc35c59236a852e4b23836708cfd3f43d1eaeed5"}, 909 | {file = "matplotlib-3.7.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a10428d4f8d1a478ceabd652e61a175b2fdeed4175ab48da4a7b8deb561e3fa"}, 910 | {file = "matplotlib-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46ca923e980f76d34c1c633343a72bb042d6ba690ecc649aababf5317997171d"}, 911 | {file = "matplotlib-3.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c849aa94ff2a70fb71f318f48a61076d1205c6013b9d3885ade7f992093ac434"}, 912 | {file = "matplotlib-3.7.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:827e78239292e561cfb70abf356a9d7eaf5bf6a85c97877f254009f20b892f89"}, 913 | {file = "matplotlib-3.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:691ef1f15360e439886186d0db77b5345b24da12cbc4fc57b26c4826db4d6cab"}, 914 | {file = "matplotlib-3.7.0-cp311-cp311-win32.whl", hash = "sha256:21a8aeac39b4a795e697265d800ce52ab59bdeb6bb23082e2d971f3041074f02"}, 915 | {file = "matplotlib-3.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:01681566e95b9423021b49dea6a2395c16fa054604eacb87f0f4c439750f9114"}, 916 | {file = "matplotlib-3.7.0-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:cf119eee4e57389fba5ac8b816934e95c256535e55f0b21628b4205737d1de85"}, 917 | {file = "matplotlib-3.7.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:21bd4033c40b95abd5b8453f036ed5aa70856e56ecbd887705c37dce007a4c21"}, 918 | {file = "matplotlib-3.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:111ef351f28fd823ed7177632070a6badd6f475607122bc9002a526f2502a0b5"}, 919 | {file = "matplotlib-3.7.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f91d35b3ef51d29d9c661069b9e4ba431ce283ffc533b981506889e144b5b40e"}, 920 | {file = "matplotlib-3.7.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a776462a4a63c0bfc9df106c15a0897aa2dbab6795c693aa366e8e283958854"}, 921 | {file = "matplotlib-3.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dfd4a0cbd151f6439e6d7f8dca5292839ca311e7e650596d073774847ca2e4f"}, 922 | {file = "matplotlib-3.7.0-cp38-cp38-win32.whl", hash = "sha256:56b7b79488209041a9bf7ddc34f1b069274489ce69e34dc63ae241d0d6b4b736"}, 923 | {file = "matplotlib-3.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:8665855f3919c80551f377bc16df618ceabf3ef65270bc14b60302dce88ca9ab"}, 924 | {file = "matplotlib-3.7.0-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:f910d924da8b9fb066b5beae0b85e34ed1b6293014892baadcf2a51da1c65807"}, 925 | {file = "matplotlib-3.7.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cf6346644e8fe234dc847e6232145dac199a650d3d8025b3ef65107221584ba4"}, 926 | {file = "matplotlib-3.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d1e52365d8d5af699f04581ca191112e1d1220a9ce4386b57d807124d8b55e6"}, 927 | {file = "matplotlib-3.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c869b646489c6a94375714032e5cec08e3aa8d3f7d4e8ef2b0fb50a52b317ce6"}, 928 | {file = "matplotlib-3.7.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4ddac5f59e78d04b20469bc43853a8e619bb6505c7eac8ffb343ff2c516d72f"}, 929 | {file = "matplotlib-3.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0304c1cd802e9a25743414c887e8a7cd51d96c9ec96d388625d2cd1c137ae3"}, 930 | {file = "matplotlib-3.7.0-cp39-cp39-win32.whl", hash = "sha256:a06a6c9822e80f323549c6bc9da96d4f233178212ad9a5f4ab87fd153077a507"}, 931 | {file = "matplotlib-3.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb52aa97b92acdee090edfb65d1cb84ea60ab38e871ba8321a10bbcebc2a3540"}, 932 | {file = "matplotlib-3.7.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3493b48e56468c39bd9c1532566dff3b8062952721b7521e1f394eb6791495f4"}, 933 | {file = "matplotlib-3.7.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d0dcd1a0bf8d56551e8617d6dc3881d8a1c7fb37d14e5ec12cbb293f3e6170a"}, 934 | {file = "matplotlib-3.7.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51fb664c37714cbaac69c16d6b3719f517a13c96c3f76f4caadd5a0aa7ed0329"}, 935 | {file = "matplotlib-3.7.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4497d88c559b76da320b7759d64db442178beeea06a52dc0c629086982082dcd"}, 936 | {file = "matplotlib-3.7.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9d85355c48ef8b9994293eb7c00f44aa8a43cad7a297fbf0770a25cdb2244b91"}, 937 | {file = "matplotlib-3.7.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03eb2c8ff8d85da679b71e14c7c95d16d014c48e0c0bfa14db85f6cdc5c92aad"}, 938 | {file = "matplotlib-3.7.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71b751d06b2ed1fd017de512d7439c0259822864ea16731522b251a27c0b2ede"}, 939 | {file = "matplotlib-3.7.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b51ab8a5d5d3bbd4527af633a638325f492e09e45e78afdf816ef55217a09664"}, 940 | {file = "matplotlib-3.7.0.tar.gz", hash = "sha256:8f6efd313430d7ef70a38a3276281cb2e8646b3a22b3b21eb227da20e15e6813"}, 941 | ] 942 | mccabe = [ 943 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 944 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 945 | ] 946 | mypy-extensions = [ 947 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 948 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 949 | ] 950 | networkx = [ 951 | {file = "networkx-3.0-py3-none-any.whl", hash = "sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e"}, 952 | {file = "networkx-3.0.tar.gz", hash = "sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412"}, 953 | ] 954 | numpy = [ 955 | {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"}, 956 | {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"}, 957 | {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"}, 958 | {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"}, 959 | {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"}, 960 | {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"}, 961 | {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"}, 962 | {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"}, 963 | {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"}, 964 | {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"}, 965 | {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"}, 966 | {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"}, 967 | {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"}, 968 | {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"}, 969 | {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"}, 970 | {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"}, 971 | {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"}, 972 | {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"}, 973 | {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"}, 974 | {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"}, 975 | {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"}, 976 | {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"}, 977 | {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"}, 978 | {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"}, 979 | {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"}, 980 | {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"}, 981 | {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"}, 982 | {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"}, 983 | ] 984 | packaging = [ 985 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, 986 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, 987 | ] 988 | pathspec = [ 989 | {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, 990 | {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, 991 | ] 992 | pillow = [ 993 | {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, 994 | {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, 995 | {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, 996 | {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, 997 | {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, 998 | {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, 999 | {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, 1000 | {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, 1001 | {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, 1002 | {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, 1003 | {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, 1004 | {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, 1005 | {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, 1006 | {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, 1007 | {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, 1008 | {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, 1009 | {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, 1010 | {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, 1011 | {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, 1012 | {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, 1013 | {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, 1014 | {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, 1015 | {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, 1016 | {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, 1017 | {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, 1018 | {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, 1019 | {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, 1020 | {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, 1021 | {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, 1022 | {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, 1023 | {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, 1024 | {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, 1025 | {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, 1026 | {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, 1027 | {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, 1028 | {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, 1029 | {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, 1030 | {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, 1031 | {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, 1032 | {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, 1033 | {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, 1034 | {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, 1035 | {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, 1036 | {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, 1037 | {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, 1038 | {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, 1039 | {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, 1040 | {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, 1041 | {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, 1042 | {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, 1043 | {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, 1044 | {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, 1045 | {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, 1046 | {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, 1047 | {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, 1048 | {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, 1049 | {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, 1050 | {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, 1051 | {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, 1052 | {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, 1053 | {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, 1054 | {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, 1055 | {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, 1056 | {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, 1057 | {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, 1058 | {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, 1059 | {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, 1060 | {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, 1061 | {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, 1062 | {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, 1063 | {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, 1064 | {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, 1065 | {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, 1066 | {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, 1067 | {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, 1068 | {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, 1069 | {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, 1070 | ] 1071 | platformdirs = [ 1072 | {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, 1073 | {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, 1074 | ] 1075 | pluggy = [ 1076 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1077 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1078 | ] 1079 | pycodestyle = [ 1080 | {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, 1081 | {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, 1082 | ] 1083 | pyflakes = [ 1084 | {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, 1085 | {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, 1086 | ] 1087 | pyparsing = [ 1088 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 1089 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 1090 | ] 1091 | pytest = [ 1092 | {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, 1093 | {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, 1094 | ] 1095 | pytest-cov = [ 1096 | {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, 1097 | {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, 1098 | ] 1099 | python-dateutil = [ 1100 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1101 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1102 | ] 1103 | pyyaml = [ 1104 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 1105 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 1106 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 1107 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 1108 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 1109 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 1110 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 1111 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 1112 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 1113 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 1114 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 1115 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 1116 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 1117 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 1118 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 1119 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 1120 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 1121 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 1122 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 1123 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 1124 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 1125 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 1126 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 1127 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 1128 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 1129 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 1130 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 1131 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 1132 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 1133 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 1134 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 1135 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 1136 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 1137 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 1138 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 1139 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 1140 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 1141 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 1142 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 1143 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 1144 | ] 1145 | setuptools = [ 1146 | {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, 1147 | {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, 1148 | ] 1149 | setuptools-scm = [ 1150 | {file = "setuptools_scm-7.1.0-py3-none-any.whl", hash = "sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e"}, 1151 | {file = "setuptools_scm-7.1.0.tar.gz", hash = "sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27"}, 1152 | ] 1153 | six = [ 1154 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1155 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1156 | ] 1157 | sqlalchemy = [ 1158 | {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b67d6e626caa571fb53accaac2fba003ef4f7317cb3481e9ab99dad6e89a70d6"}, 1159 | {file = "SQLAlchemy-2.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b01dce097cf6f145da131a53d4cce7f42e0bfa9ae161dd171a423f7970d296d0"}, 1160 | {file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738c80705e11c1268827dbe22c01162a9cdc98fc6f7901b429a1459db2593060"}, 1161 | {file = "SQLAlchemy-2.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6363697c938b9a13e07f1bc2cd433502a7aa07efd55b946b31d25b9449890621"}, 1162 | {file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a42e6831e82dfa6d16b45f0c98c69e7b0defc64d76213173456355034450c414"}, 1163 | {file = "SQLAlchemy-2.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:011ef3c33f30bae5637c575f30647e0add98686642d237f0c3a1e3d9b35747fa"}, 1164 | {file = "SQLAlchemy-2.0.4-cp310-cp310-win32.whl", hash = "sha256:c1e8edc49b32483cd5d2d015f343e16be7dfab89f4aaf66b0fa6827ab356880d"}, 1165 | {file = "SQLAlchemy-2.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:77a380bf8721b416782c763e0ff66f80f3b05aee83db33ddfc0eac20bcb6791f"}, 1166 | {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a2f9120eb32190bdba31d1022181ef08f257aed4f984f3368aa4e838de72bc0"}, 1167 | {file = "SQLAlchemy-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:679b9bd10bb32b8d3befed4aad4356799b6ec1bdddc0f930a79e41ba5b084124"}, 1168 | {file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:582053571125895d008d4b8d9687d12d4bd209c076cdbab3504da307e2a0a2bd"}, 1169 | {file = "SQLAlchemy-2.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c82395e2925639e6d320592943608070678e7157bd1db2672a63be9c7889434"}, 1170 | {file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:25e4e54575f9d2af1eab82d3a470fca27062191c48ee57b6386fe09a3c0a6a33"}, 1171 | {file = "SQLAlchemy-2.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9946ee503962859f1a9e1ad17dff0859269b0cb453686747fe87f00b0e030b34"}, 1172 | {file = "SQLAlchemy-2.0.4-cp311-cp311-win32.whl", hash = "sha256:c621f05859caed5c0aab032888a3d3bde2cae3988ca151113cbecf262adad976"}, 1173 | {file = "SQLAlchemy-2.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:662a79e80f3e9fe33b7861c19fedf3d8389fab2413c04bba787e3f1139c22188"}, 1174 | {file = "SQLAlchemy-2.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3f927340b37fe65ec42e19af7ce15260a73e11c6b456febb59009bfdfec29a35"}, 1175 | {file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67901b91bf5821482fcbe9da988cb16897809624ddf0fde339cd62365cc50032"}, 1176 | {file = "SQLAlchemy-2.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1644c603558590f465b3fa16e4557d87d3962bc2c81fd7ea85b582ecf4676b31"}, 1177 | {file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9a7ecaf90fe9ec8e45c86828f4f183564b33c9514e08667ca59e526fea63893a"}, 1178 | {file = "SQLAlchemy-2.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8a88b32ce5b69d18507ffc9f10401833934ebc353c7b30d1e056023c64f0a736"}, 1179 | {file = "SQLAlchemy-2.0.4-cp37-cp37m-win32.whl", hash = "sha256:2267c004e78e291bba0dc766a9711c389649cf3e662cd46eec2bc2c238c637bd"}, 1180 | {file = "SQLAlchemy-2.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:59cf0cdb29baec4e074c7520d7226646a8a8f856b87d8300f3e4494901d55235"}, 1181 | {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd801375f19a6e1f021dabd8b1714f2fdb91cbc835cd13b5dd0bd7e9860392d7"}, 1182 | {file = "SQLAlchemy-2.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8efdda920988bcade542f53a2890751ff680474d548f32df919a35a21404e3f"}, 1183 | {file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:918c2b553e3c78268b187f70983c9bc6f91e451a4f934827e9c919e03d258bd7"}, 1184 | {file = "SQLAlchemy-2.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d05773d5c79f2d3371d81697d54ee1b2c32085ad434ce9de4482e457ecb018"}, 1185 | {file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fdb2686eb01f670cdc6c43f092e333ff08c1cf0b646da5256c1237dc4ceef4ae"}, 1186 | {file = "SQLAlchemy-2.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ff0a7c669ec7cdb899eae7e622211c2dd8725b82655db2b41740d39e3cda466"}, 1187 | {file = "SQLAlchemy-2.0.4-cp38-cp38-win32.whl", hash = "sha256:57dcd9eed52413f7270b22797aa83c71b698db153d1541c1e83d45ecdf8e95e7"}, 1188 | {file = "SQLAlchemy-2.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:54aa9f40d88728dd058e951eeb5ecc55241831ba4011e60c641738c1da0146b7"}, 1189 | {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:817aab80f7e8fe581696dae7aaeb2ceb0b7ea70ad03c95483c9115970d2a9b00"}, 1190 | {file = "SQLAlchemy-2.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc7b9f55c2f72c13b2328b8a870ff585c993ba1b5c155ece5c9d3216fa4b18f6"}, 1191 | {file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f696828784ab2c07b127bfd2f2d513f47ec58924c29cff5b19806ac37acee31c"}, 1192 | {file = "SQLAlchemy-2.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce54965a94673a0ebda25e7c3a05bf1aa74fd78cc452a1a710b704bf73fb8402"}, 1193 | {file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f342057422d6bcfdd4996e34cd5c7f78f7e500112f64b113f334cdfc6a0c593d"}, 1194 | {file = "SQLAlchemy-2.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5deafb4901618b3f98e8df7099cd11edd0d1e6856912647e28968b803de0dae"}, 1195 | {file = "SQLAlchemy-2.0.4-cp39-cp39-win32.whl", hash = "sha256:81f1ea264278fcbe113b9a5840f13a356cb0186e55b52168334124f1cd1bc495"}, 1196 | {file = "SQLAlchemy-2.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:954f1ad73b78ea5ba5a35c89c4a5dfd0f3a06c17926503de19510eb9b3857bde"}, 1197 | {file = "SQLAlchemy-2.0.4-py3-none-any.whl", hash = "sha256:0adca8a3ca77234a142c5afed29322fb501921f13d1d5e9fa4253450d786c160"}, 1198 | {file = "SQLAlchemy-2.0.4.tar.gz", hash = "sha256:95a18e1a6af2114dbd9ee4f168ad33070d6317e11bafa28d983cc7b585fe900b"}, 1199 | ] 1200 | tomli = [ 1201 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1202 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1203 | ] 1204 | typer = [ 1205 | {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"}, 1206 | {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"}, 1207 | ] 1208 | typing-extensions = [ 1209 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, 1210 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, 1211 | ] 1212 | zipp = [ 1213 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 1214 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 1215 | ] 1216 | -------------------------------------------------------------------------------- /pydwt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mg30/pydwt/28b82aaa3f77f29de29a659c04b388afceb39aaa/pydwt/__init__.py -------------------------------------------------------------------------------- /pydwt/app.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import Dict 3 | import os 4 | import sys 5 | from typing import Optional 6 | import typer 7 | import yaml 8 | from dependency_injector.wiring import register_loader_containers 9 | from pydwt.core.containers import Container 10 | import logging 11 | 12 | config_file = "settings.yml" 13 | sys.path.append(os.getcwd()) 14 | app = typer.Typer() 15 | container = Container() 16 | register_loader_containers(container) 17 | 18 | 19 | def load_config(path: str) -> Dict: 20 | config = None 21 | with open(path) as f: 22 | config = yaml.safe_load(f) 23 | return config 24 | 25 | 26 | # Define command-line interface using Typer 27 | @app.command() 28 | def new(project_name: str): 29 | """Create a new pydwt project.""" 30 | project_handler = container.project_factory() 31 | project_handler.new(project_name) 32 | 33 | 34 | @app.command() 35 | def run( 36 | name: Optional[str] = typer.Argument(None), 37 | with_dep: bool = typer.Option(False, "--with-dep"), 38 | ): 39 | """Run the workflow DAG for the current project.""" 40 | config = load_config(path=config_file) 41 | container.config.from_dict(config) 42 | project_handler = container.project_factory() 43 | project_handler.run(name, with_dep) 44 | 45 | 46 | @app.command() 47 | def export_dag(): 48 | """Export the workflow DAG for the current project.""" 49 | config = load_config(path=config_file) 50 | container.config.from_dict(config) 51 | project_handler = container.project_factory() 52 | project_handler.export_dag() 53 | 54 | 55 | @app.command() 56 | def test_connection(): 57 | """Export the workflow DAG for the current project.""" 58 | config = load_config(path=config_file) 59 | container.config.from_dict(config) 60 | try: 61 | conn = container.database_client() 62 | engine = conn.get_engine() 63 | dbapi = engine.connect() 64 | dbapi.close() 65 | logging.info("successfully connected to db") 66 | except Exception: 67 | logging.error(f"connection failed {traceback.print_exc()}") 68 | 69 | 70 | if __name__ == "__main__": 71 | # Run the command-line interface 72 | try: 73 | app() 74 | except Exception as e: 75 | # Display a helpful error message if a known error occurs 76 | if "config" in str(e): 77 | print("Error: Failed to load configuration from settings.yml.") 78 | print("Make sure the file exists and contains valid YAML data.") 79 | else: 80 | # Display the full error message for unknown errors 81 | print(f"Error: {e}") 82 | -------------------------------------------------------------------------------- /pydwt/context/connection.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Dict 3 | from sqlalchemy import create_engine, Engine 4 | 5 | 6 | @dataclass 7 | class Connection(object): 8 | """Class representing a connection to a database. 9 | 10 | Attributes: 11 | params (Dict): containning SQL alchemy DB url and Kwargs forwarded 12 | to create_engine. 13 | 14 | engine (sqlalchemy.engine.Engine): Database engine. 15 | """ 16 | 17 | params: Dict 18 | engine: Engine = field(init=False, default=None) 19 | 20 | def get_engine(self): 21 | return create_engine(**self.params) 22 | -------------------------------------------------------------------------------- /pydwt/context/datasources.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pydwt.sql.session import Session 3 | from pydwt.context.connection import Connection 4 | from typing import Dict 5 | 6 | 7 | @dataclass 8 | class Datasources(object): 9 | """Class for accessing tables in a SQL database. 10 | 11 | Attributes: 12 | referentiel (dict): A dictionary that contains metadata about the database. 13 | engine (Any): A SQLAlchemy engine object that connects to the database. 14 | """ 15 | 16 | referentiel: Dict[str, dict] 17 | connection: Connection 18 | 19 | def get_source(self, name: str): 20 | """Returns a SQLAlchemy Table object for a given data source. 21 | 22 | Args: 23 | name (str): The name of the data source to retrieve. 24 | 25 | Returns: 26 | DataFrame: A DataFrame object representing the data source. 27 | """ 28 | # Get the configuration for the data source 29 | config = self.referentiel[name] 30 | engine = self.connection.get_engine() 31 | # Create a Session object for the schema that contains the table 32 | session = Session(engine=engine, schema=config["schema"]) 33 | 34 | # Return a Table object for the table in the data source 35 | return session.table(config["table"]) 36 | -------------------------------------------------------------------------------- /pydwt/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mg30/pydwt/28b82aaa3f77f29de29a659c04b388afceb39aaa/pydwt/core/__init__.py -------------------------------------------------------------------------------- /pydwt/core/containers.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is defining a dependency injection container 3 | using the Dependency Injector library. 4 | 5 | The container has several providers which can be used 6 | to inject objects into other parts of the codebase. 7 | config is a configuration provider that should be used to provide 8 | a dictionary of configuration settings. 9 | database_client is a Connection provider which is used to create 10 | a database connection with the configuration provided. 11 | datasources is a provider that returns a Datasources instance, 12 | which can be used to retrieve tables or views from the database. 13 | cache_strategy is a provider that should be used to provide a cache strategy instance. 14 | workflow_factory is a provider that returns a Workflow instance, 15 | which is used to execute tasks for a given DAG. 16 | project_factory is a provider that returns a Project instance, 17 | which is a collection of tasks that can be executed together as a single unit. 18 | """ 19 | 20 | from dependency_injector import containers, providers 21 | from pydwt.context.connection import Connection 22 | from pydwt.core.workflow import Workflow 23 | from pydwt.core.dag import Dag 24 | from pydwt.core.project import Project 25 | from pydwt.context.datasources import Datasources 26 | from pydwt.core.executors import ThreadExecutor 27 | 28 | 29 | class Container(containers.DeclarativeContainer): 30 | # Configuration provider, contains the project configuration 31 | config = providers.Configuration() 32 | 33 | # Singleton provider that provides the database connection instance 34 | database_client = providers.ThreadSafeSingleton( 35 | Connection, 36 | config.connection, 37 | ) 38 | 39 | # Singleton provider that provides the datasources instance 40 | datasources = providers.ThreadSafeSingleton( 41 | Datasources, config.sources, database_client 42 | ) 43 | 44 | dag_factory = providers.ThreadSafeSingleton(Dag) 45 | 46 | executor_factory = providers.Factory(ThreadExecutor, nb_workers=5, dag=dag_factory) 47 | 48 | # Singleton provider that provides the workflow instance 49 | workflow_factory = providers.ThreadSafeSingleton( 50 | Workflow, dag=dag_factory, executor=executor_factory 51 | ) 52 | 53 | # Factory provider that provides the project instance 54 | project_factory = providers.Factory( 55 | Project, workflow=workflow_factory, name=config.project.name 56 | ) 57 | -------------------------------------------------------------------------------- /pydwt/core/dag.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from typing import Dict 3 | from pydwt.core.enums import Status 4 | import logging 5 | 6 | 7 | class Dag(object): 8 | """DAG class to handle graph creation, traversal and saving the output. 9 | 10 | Attributes: 11 | tasks (List): List of tasks to build the dag from. 12 | graph (nx.DiGraph): Directed Graph that holds the task relationships. 13 | """ 14 | 15 | def __init__(self) -> None: 16 | """Build the DAG after object initialization.""" 17 | self._tasks = [] 18 | self.graph = nx.DiGraph() 19 | self.source = "s" 20 | self.node_index = {} 21 | self.node_names = {} 22 | 23 | @property 24 | def tasks(self): 25 | return self._tasks 26 | 27 | @tasks.setter 28 | def tasks(self, value): 29 | self._tasks = value 30 | 31 | def build_dag(self) -> None: 32 | """Build the directed acyclic graph from the tasks and their dependencies.""" 33 | edges = [] 34 | 35 | for i, task in enumerate(self.tasks): 36 | self.node_names[i] = task.name 37 | self.node_index[task.name] = i 38 | self.graph.add_node(i, name=task.name) 39 | 40 | if task.depends_on: 41 | for dep_func in task.depends_on: 42 | dep_name = f"{dep_func.__module__}.{dep_func.__name__}" 43 | if dep_name in self.node_names.values(): 44 | dep_index = next( 45 | index 46 | for index, name in self.node_names.items() 47 | if name == dep_name 48 | ) 49 | else: 50 | dep_index = len(self.node_names) 51 | self.node_names[dep_index] = dep_name 52 | self.graph.add_node(dep_index, name=dep_name) 53 | 54 | edges.append((dep_index, i)) 55 | else: 56 | edges.append((self.source, i)) 57 | 58 | self.graph.add_edges_from(edges) 59 | 60 | def build_level(self, target: str = None) -> Dict: 61 | """Assign levels to nodes in the dag using the breadth-first search. 62 | 63 | Args: 64 | target (str): Optional target node. If provided, 65 | the search will be performed up to this node. 66 | 67 | Returns: 68 | Dict: Dictionary of levels and their corresponding node indexes. 69 | 70 | Raises: 71 | KeyError: If target node is not found in the graph. 72 | """ 73 | nodes_by_level = {} 74 | # If a target node is provided, get its index in the node_index dictionary 75 | node_index = self.node_index.get(target, None) 76 | # Perform breadth-first search on the graph 77 | bfs_tree = nx.bfs_tree(self.graph, self.source) 78 | 79 | level = None 80 | 81 | if node_index is not None: 82 | level = {} 83 | path = nx.shortest_path(self.graph, self.source, node_index) 84 | for i, node in enumerate(path): 85 | level[node] = i 86 | 87 | else: 88 | # Assign levels to nodes based on their distance from the root node 89 | level = nx.shortest_path_length(bfs_tree, self.source) 90 | for node, node_level in level.items(): 91 | if node_level not in nodes_by_level: 92 | nodes_by_level[node_level] = [] 93 | nodes_by_level[node_level].append(node) 94 | return nodes_by_level 95 | 96 | def check_parents_status(self, task): 97 | """Check if all parent tasks have the attribute status set to success. 98 | 99 | Args: 100 | task: The task to check. 101 | 102 | Returns: 103 | bool: True if all parent tasks have the attribute status set to success, 104 | False otherwise. 105 | """ 106 | node_index = self.node_index[task.name] 107 | logging.debug(f"check parent for task {task.name}\n") 108 | logging.debug(list(self.graph.predecessors(node_index)), end="\n") 109 | for parent_index in self.graph.predecessors(node_index): 110 | if parent_index == "s": 111 | continue 112 | logging.debug(f"parent index is {parent_index}") 113 | parent = self.tasks[parent_index] 114 | logging.debug(f"parent {parent.name}", end="\n") 115 | if parent.status == Status.ERROR: 116 | logging.debug("parent is error", end="\n") 117 | return Status.ERROR 118 | elif parent.status == Status.PENDING: 119 | logging.debug("parent is pending", end="\n") 120 | return Status.PENDING 121 | logging.debug("parent is success") 122 | return Status.SUCCESS 123 | 124 | def filter_dag(self, task_name: str) -> None: 125 | """ 126 | Filters the DAG by removing all nodes that 127 | are not reachable from the given task. 128 | 129 | Args: 130 | task_name: a string representing the name 131 | of the task to start the filtering from 132 | 133 | Returns: 134 | None 135 | """ 136 | node_index = self.node_index[task_name] 137 | 138 | nodes = nx.ancestors(self.graph, node_index) | {node_index} 139 | nodes = list(nodes) 140 | 141 | filtred_tasks = [] 142 | 143 | for n in nodes: 144 | if n != "s": 145 | task = self.tasks[n] 146 | filtred_tasks.append(task) 147 | 148 | self.tasks = filtred_tasks 149 | self.build_dag() 150 | 151 | -------------------------------------------------------------------------------- /pydwt/core/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class Status(enum.Enum): 5 | ERROR = 0 6 | SUCCESS = 1 7 | PENDING = 2 8 | -------------------------------------------------------------------------------- /pydwt/core/executors.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import threading 3 | from abc import ABC 4 | from dataclasses import dataclass, field 5 | from typing import Any, List 6 | from pydwt.core.enums import Status 7 | import logging 8 | 9 | 10 | class AbstractExecutor(ABC): 11 | nb_workers: int 12 | _queue: Any 13 | _tasks: List = field(init=False) 14 | 15 | @property 16 | def tasks(self): 17 | return self._tasks 18 | 19 | @tasks.setter 20 | def tasks(self, value): 21 | self._tasks = value 22 | 23 | def run(self): 24 | raise NotImplementedError 25 | 26 | def worker(self): 27 | raise NotImplementedError 28 | 29 | 30 | @dataclass 31 | class ThreadExecutor(AbstractExecutor): 32 | dag: Any 33 | nb_workers: int = 2 34 | _queue: queue.Queue = field(init=False, default_factory=queue.Queue) 35 | 36 | def run(self) -> None: 37 | """Run all workers""" 38 | for task in self.tasks: 39 | self._queue.put(task) 40 | 41 | for _ in range(0, self.nb_workers): 42 | threading.Thread(target=self.worker, daemon=True).start() 43 | self._queue.join() 44 | 45 | def worker(self) -> None: 46 | """Pull a task from the queue and process""" 47 | while not self._queue.empty(): 48 | task = self._queue.get() 49 | try: 50 | parents_status = self.dag.check_parents_status(task) 51 | if parents_status == Status.ERROR: 52 | logging.error( 53 | f"task {task.name} can not be run because\ 54 | some parent are in ERROR" 55 | ) 56 | task.status = Status.ERROR 57 | elif parents_status == Status.PENDING: 58 | logging.info(f"task {task.name} is pending") 59 | self._queue.put(task) 60 | else: 61 | task.run() 62 | except Exception as e: 63 | logging.error(f"task {task.name} failed with error: {e}") 64 | task.status = Status.ERROR 65 | finally: 66 | self._queue.task_done() 67 | -------------------------------------------------------------------------------- /pydwt/core/project.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import logging 3 | import os 4 | import sys 5 | from dataclasses import dataclass, field 6 | from datetime import datetime 7 | from typing import Dict 8 | 9 | import yaml 10 | 11 | from pydwt.core.workflow import Workflow 12 | 13 | 14 | @dataclass 15 | class Project: 16 | """ 17 | Project class to manage the DAG-based workflows. 18 | 19 | Attributes: 20 | workflow (Workflow): Workflow object to execute DAG. 21 | name (str): Name of the project. 22 | models_folder (str): Name of the folder that 23 | contains the models (default: "models"). 24 | dags_folder (str): Name of the folder to store 25 | the DAGs (default: "dags"). 26 | """ 27 | 28 | workflow: Workflow 29 | name: str 30 | models_folder: str = field(default="models") 31 | dags_folder: str = field(default="dags") 32 | 33 | def __post_init__(self) -> None: 34 | # Add the current working directory to the system path 35 | # to allow importing modules from the project. 36 | sys.path.append(os.getcwd()) 37 | 38 | # Configure the logging. 39 | logging.basicConfig( 40 | format="%(levelname)s %(asctime)s: %(message)s", 41 | datefmt="%m/%d/%Y %I:%M:%S %p", 42 | level=logging.INFO, 43 | ) 44 | 45 | def new(self, project_name: str) -> None: 46 | """ 47 | Create a new project directory structure. 48 | 49 | Args: 50 | project_name (str): Name of the project. 51 | """ 52 | self._create_models_directory(project_name) 53 | self._create_tasks_example(project_name) 54 | self._create_dags_directory(project_name) 55 | self._create_settings(project_name) 56 | 57 | def import_all_models(self) -> None: 58 | """Import all the models from the models folder of the project.""" 59 | models_folders = os.path.join(self.name, "models") 60 | models = [ 61 | file.split(".")[0] 62 | for file in os.listdir(models_folders) 63 | if file.endswith(".py") 64 | ] 65 | for model in models: 66 | importlib.import_module(f"{self.name}.models.{model}") 67 | 68 | def run(self, task_name: str = None, with_dep: bool = False) -> None: 69 | """Run the DAG-based workflow.""" 70 | 71 | task_full_name = f"{self.name}.models.{task_name}" if task_name else None 72 | self.import_all_models() 73 | if task_name and with_dep: 74 | self.workflow.run_with_name_and_deps(task_full_name) 75 | 76 | elif task_name and not with_dep: 77 | self.workflow.run_with_name_no_deps(task_full_name) 78 | 79 | elif not task_name and not with_dep: 80 | self.workflow.run() 81 | else: 82 | raise ValueError("with-dep must be with a task-name") 83 | 84 | def export_dag(self) -> None: 85 | """Export the DAG to a PNG image file.""" 86 | self.import_all_models() 87 | dag_file_name = os.path.join( 88 | self.name, 89 | self.dags_folder, 90 | f'dag_{datetime.now().strftime("%Y%m%d_%H:%M:%S")}', 91 | ) 92 | self.workflow.dag.build_dag() 93 | self.workflow.export_dag(dag_file_name) 94 | 95 | def _create_models_directory(self, project_name: str) -> None: 96 | """ 97 | Create the models directory if it does not exist. 98 | 99 | Args: 100 | project_name (str): Name of the project. 101 | """ 102 | models_project = os.path.join(project_name, self.models_folder) 103 | if not os.path.exists(models_project): 104 | os.makedirs(models_project) 105 | 106 | def _create_dags_directory(self, project_name: str) -> None: 107 | """ 108 | Create the DAGs directory if it does not exist. 109 | 110 | Args: 111 | project_name (str): Name of the project. 112 | """ 113 | dag_folder = os.path.join(project_name, self.dags_folder) 114 | if not os.path.exists(dag_folder): 115 | os.makedirs(dag_folder) 116 | 117 | def _create_settings(self, project_name: str) -> None: 118 | """Create a default settings file for the project. 119 | 120 | Args: 121 | project_name (str): Name of the project to create settings for. 122 | """ 123 | settings_projects = os.path.join("settings.yml") 124 | settings: Dict = { 125 | "project": { 126 | "name": project_name, 127 | }, 128 | "tasks": {"task_one": {"materialize": "view"}}, 129 | "sources": {"one": {"table": "table_name", "schema": "some_schema"}}, 130 | "connection": {"url": "", "echo": True}, 131 | } 132 | if not os.path.exists(settings_projects): 133 | with open(settings_projects, "w") as file: 134 | yaml.safe_dump(settings, file) 135 | 136 | def _create_tasks_example(self, project_name: str) -> None: 137 | """Create an example task file for the project. 138 | 139 | Args: 140 | project_name (str): Name of the project to create the example task for. 141 | """ 142 | models_project = os.path.join(project_name, self.models_folder) 143 | example_file = os.path.join(models_project, "example.py") 144 | if not os.path.exists(example_file): 145 | with open(example_file, "w") as f: 146 | f.write( 147 | """ 148 | from pydwt.core.task import Task 149 | from dependency_injector.wiring import inject, Provide 150 | from pydwt.core.containers import Container 151 | 152 | @Task() 153 | @inject 154 | def task_one(config:dict = Provide[Container.config.tasks.task_one]): 155 | print(config) 156 | 157 | @Task(depends_on=[task_one]) 158 | def task_two(): 159 | print("somme processing") 160 | 161 | """ 162 | ) 163 | -------------------------------------------------------------------------------- /pydwt/core/schedule.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module that provide API to create classe that implements schedule 3 | intervals 4 | """ 5 | import datetime 6 | from abc import ABC, abstractmethod 7 | from calendar import Calendar 8 | from dataclasses import dataclass, field 9 | 10 | 11 | @dataclass 12 | class ScheduleInterface(ABC): 13 | calendar: Calendar = field(default_factory=Calendar) 14 | 15 | @abstractmethod 16 | def is_scheduled(self, date: datetime = datetime.datetime.now()): 17 | """Abstract method to be implemented by child class defining the schedule 18 | time to run the script. 19 | """ 20 | raise NotImplementedError 21 | 22 | 23 | class Daily(ScheduleInterface): 24 | """Provide a daily implementation of schedule interface""" 25 | 26 | def is_scheduled(self, date: datetime.datetime = datetime.datetime.now()): 27 | return True 28 | 29 | 30 | @dataclass 31 | class Weekly(ScheduleInterface): 32 | """Provide a weekly implementation of schedule interface""" 33 | 34 | weekday: int = 0 35 | 36 | def is_scheduled(self, date: datetime.datetime = datetime.datetime.now()): 37 | return self.weekday == date.weekday() 38 | 39 | 40 | @dataclass 41 | class SemiMonthly(ScheduleInterface): 42 | """Implementation of schedule interface checking if a date is in the 43 | two first weekday in the month 44 | """ 45 | 46 | weekday: int = 0 47 | 48 | def is_scheduled(self, date: datetime.datetime = datetime.datetime.now()): 49 | weeksofmonth = self.calendar.monthdatescalendar(date.year, date.month) 50 | dates_to_check = [ 51 | d 52 | for week in weeksofmonth 53 | for d in week 54 | if d.weekday() == self.weekday and d.month == date.month 55 | ][::2] 56 | return date.date() in dates_to_check 57 | 58 | 59 | @dataclass 60 | class Monthly(ScheduleInterface): 61 | """Implementation of schedule interface checking if a date is in the 62 | first weekday in the month 63 | """ 64 | 65 | weekday: int = 0 66 | 67 | def is_scheduled(self, date: datetime.datetime = datetime.datetime.now()): 68 | weeksofmonth = self.calendar.monthdatescalendar(date.year, date.month) 69 | date_to_check = [ 70 | d 71 | for week in weeksofmonth 72 | for d in week 73 | if d.weekday() == self.weekday and d.month == date.month 74 | ][0] 75 | return date.date() == date_to_check 76 | 77 | 78 | @dataclass 79 | class MonthlyLastOpenDayInMonth(ScheduleInterface): 80 | """Implementation of schedule interface checking if a date is the last weekday [0-4] 81 | of the month. 82 | """ 83 | 84 | def is_scheduled(self, date: datetime.datetime = datetime.datetime.now()): 85 | weeksofmonth = self.calendar.monthdatescalendar(date.year, date.month) 86 | last_week = weeksofmonth[-1] 87 | date_to_check = [ 88 | d for d in last_week if d.weekday() not in [5, 6] and d.month == date.month 89 | ][-1] 90 | return date.date() == date_to_check 91 | -------------------------------------------------------------------------------- /pydwt/core/task.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import traceback 4 | import time 5 | 6 | from abc import ABC, abstractmethod 7 | from dataclasses import dataclass, field 8 | from typing import Callable, Dict, List 9 | 10 | from dependency_injector.wiring import Provide 11 | 12 | from pydwt.core.containers import Container 13 | from pydwt.core.schedule import Daily, ScheduleInterface 14 | from pydwt.core.workflow import Workflow 15 | from pydwt.core.enums import Status 16 | 17 | 18 | @dataclass 19 | class BaseTask(ABC): 20 | """ 21 | Abstract base class for representing a task in a DAG. 22 | 23 | :param depends_on: List of other tasks that this task depends on 24 | :param runs_on: Schedule for running this task. Default is `Daily()` 25 | :param retry: Number of times to retry this task in case of failure 26 | :param ttl_minutes: Time-to-live in minutes. 27 | If a positive value is provided, the task will only run 28 | if the time elapsed since the last run is 29 | greater than or equal to this value. 30 | """ 31 | 32 | depends_on: List[Callable] = field(default_factory=list) 33 | runs_on: ScheduleInterface = field(default_factory=Daily) 34 | retry: int = 0 35 | name: str = field(init=False) 36 | _task: Callable = field(init=False, default=None) 37 | _count_call: int = 0 38 | workflow: Workflow = Provide[Container.workflow_factory] 39 | config: Dict = Provide[Container.config] 40 | sources: Dict = Provide[Container.datasources] 41 | status: Status = Status.PENDING 42 | 43 | @property 44 | def depends_on_name(self): 45 | return ( 46 | [f"{func.__module__}.{func.__name__}" for func in self.depends_on] 47 | if self.depends_on 48 | else [] 49 | ) 50 | 51 | @property 52 | def runs_on_name(self): 53 | return str(type(self.runs_on).__name__) 54 | 55 | def __call__(self, func: Callable): 56 | """ 57 | Decorator for registering a task in the DAG. 58 | """ 59 | self._task = func 60 | self.name = f"{func.__module__}.{func.__name__}" 61 | 62 | logging.info(f"registering task {self.name}") 63 | self.workflow.tasks.append(self) 64 | 65 | @functools.wraps(func) 66 | def wrapper(*args, **kwargs): 67 | return func(*args, **kwargs) 68 | 69 | return wrapper 70 | 71 | @abstractmethod 72 | def run(self): 73 | """ 74 | Run this task. 75 | """ 76 | raise NotImplementedError 77 | 78 | @abstractmethod 79 | def _run_task_with_retry(self): 80 | raise NotImplementedError 81 | 82 | 83 | @dataclass 84 | class Task(BaseTask): 85 | """ 86 | Class representing a task in a DAG. 87 | 88 | :param depends_on: List of other tasks that this task depends on 89 | :param runs_on: Schedule for running this task. Default is `Daily()` 90 | :param retry: Number of times to retry this task in case of failure 91 | :param ttl_minutes: Time-to-live in minutes. 92 | If a positive value is provided,the task will only run 93 | if the time elapsed since the last run is greater than or equal to this value. 94 | """ 95 | 96 | workflow: Workflow = Provide[Container.workflow_factory] 97 | config: Dict = Provide[Container.config] 98 | sources: Dict = Provide[Container.datasources] 99 | 100 | def run(self): 101 | """ 102 | Run this task. 103 | """ 104 | if not self.runs_on.is_scheduled(): 105 | logging.info(f"task {self.name} is not scheduled to be run: skipping") 106 | return 107 | 108 | logging.info(f"task {self.name} is scheduled to be run") 109 | start_time = time.time() 110 | self._run_task_with_retry() 111 | elapsed_time = time.time() - start_time 112 | logging.info(f"task {self.name} completed in {elapsed_time:.2f} seconds") 113 | 114 | def __eq__(self, other): 115 | if isinstance(other, Task): 116 | return ( 117 | set(self.depends_on_name) == set(other.depends_on_name) 118 | and self.runs_on_name == other.runs_on_name 119 | and self.retry == other.retry 120 | and self.name == other.name 121 | ) 122 | return False 123 | 124 | def _run_task_with_retry(self): 125 | self._count_call = 0 126 | for n in range(self.retry + 1): 127 | try: 128 | self._count_call += 1 129 | self._task() 130 | self.status = Status.SUCCESS 131 | break 132 | except Exception: 133 | if n == self.retry: 134 | logging.error( 135 | f"task {self.name} failed after {self.retry}\ 136 | attempts: {traceback.print_exc()}" 137 | ) 138 | self.status = Status.ERROR 139 | 140 | else: 141 | logging.info(f"retrying task {self.name} try number: {n}") 142 | -------------------------------------------------------------------------------- /pydwt/core/workflow.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from dataclasses import dataclass, field 5 | from typing import List 6 | 7 | import matplotlib.pyplot as plt 8 | import networkx as nx 9 | 10 | from pydwt.core.dag import Dag 11 | from pydwt.core.executors import AbstractExecutor 12 | 13 | 14 | @dataclass 15 | class Workflow(object): 16 | """Class for running a directed acyclic graph of tasks. 17 | 18 | Attributes: 19 | tasks (List[Type[Task]]): List of tasks to run in the DAG. 20 | dag (Dag): DAG object for the tasks. 21 | """ 22 | 23 | tasks: List = field(default_factory=list, init=False) 24 | dag: Dag 25 | executor: AbstractExecutor 26 | 27 | def __post_init__(self) -> None: 28 | """Create a DAG object after initialization.""" 29 | 30 | self.dag.tasks = self.tasks 31 | self.executor.tasks = self.tasks 32 | 33 | def run(self) -> None: 34 | """Run the tasks in the DAG.""" 35 | self.dag.build_dag() 36 | start_time_workflow = time.time() 37 | self.executor.run() 38 | elapsed_time_workflow = time.time() - start_time_workflow 39 | logging.info(f"workflow completed in {elapsed_time_workflow:.2f} seconds") 40 | 41 | def run_with_name_and_deps(self, task_name: str) -> None: 42 | """Run the tasks in the DAG.""" 43 | self.dag.build_dag() 44 | self.dag.filter_dag(task_name) 45 | 46 | self.tasks = self.dag.tasks 47 | self.executor.tasks = self.tasks 48 | 49 | start_time_workflow = time.time() 50 | self.executor.run() 51 | elapsed_time_workflow = time.time() - start_time_workflow 52 | logging.info(f"workflow completed in {elapsed_time_workflow:.2f} seconds") 53 | 54 | def run_with_name_no_deps(self, task_name: str) -> None: 55 | """Run the tasks in the DAG.""" 56 | task = next(task for task in self.tasks if task.name == task_name) 57 | task.run() 58 | 59 | def export_dag(self, path: str) -> None: 60 | """Export the DAG to a PNG image file. 61 | 62 | Args: 63 | path (str): Path to the directory where the image file should be saved. 64 | """ 65 | graph = self.dag.graph 66 | node_names = nx.get_node_attributes(graph, "name") 67 | pos = nx.spring_layout(graph) 68 | nx.draw(graph, pos=pos) 69 | nx.draw_networkx_labels(graph, pos=pos, labels=node_names) 70 | plt.savefig(f"{path}") 71 | -------------------------------------------------------------------------------- /pydwt/sql/dataframe.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List, Literal 3 | import sqlalchemy 4 | from sqlalchemy import select, join, union_all, text, column 5 | from pydwt.sql.materializations import CreateTableAs, CreateViewAs 6 | 7 | 8 | class DataFrame(dict): 9 | """DataFrame class is an interface that allows to manipulate data 10 | using SQL-like operations on top of SQLAlchemy core. 11 | 12 | Args: 13 | base (selectable): Initial SQLAlchemy selectable object. 14 | engine (Engine): SQLAlchemy engine to execute the SQL commands. 15 | 16 | Properties: 17 | columns (List[str]): Names of the columns in the dataframe. 18 | """ 19 | 20 | __getattr__ = ( 21 | dict.__getitem__ 22 | ) # overwrite `__getattr__` to allow access to values using dot notation 23 | 24 | def __init__(self, base, engine): 25 | """Initialize the DataFrame object.""" 26 | self._stmt = base 27 | self._engine = engine 28 | for col in self._stmt.columns: 29 | name = str(col).split(".")[-1] 30 | self[name] = getattr(self._stmt.c, name) 31 | 32 | @property 33 | def columns(self) -> List[str]: 34 | """Return a list of the column names in the dataframe.""" 35 | return [str(col).split(".")[-1] for col in self._stmt.columns] 36 | 37 | def select(self, *args) -> DataFrame: 38 | """Create a new DataFrame that only has the columns specified. 39 | 40 | Args: 41 | *args (selectable): Columns to select from the dataframe. 42 | 43 | Returns: 44 | DataFrame: New DataFrame with only the selected columns. 45 | """ 46 | columns = [] 47 | for arg in args: 48 | if isinstance(arg, str): 49 | columns.append(self[arg]) 50 | else: 51 | columns.append(arg) 52 | 53 | projection = select(*columns).cte() 54 | return DataFrame(projection, self._engine) 55 | 56 | def where(self, expr) -> DataFrame: 57 | """Create a new DataFrame that only has rows that meet the condition. 58 | 59 | Args: 60 | expr (sql.expression): SQLAlchemy expression to filter the rows. 61 | 62 | Returns: 63 | DataFrame: New DataFrame with only the rows that meet the condition. 64 | """ 65 | if isinstance(expr, str): 66 | filtered = select(self._stmt).where(text(expr)).cte() 67 | else: 68 | filtered = select(self._stmt).where(expr).cte() 69 | return DataFrame(filtered, self._engine) 70 | 71 | def filter(self, condition: str) -> DataFrame: 72 | """ 73 | Create a new DataFrame by filtering this DataFrame with a SQL condition. 74 | 75 | Args: 76 | condition (str): A SQL WHERE clause condition. 77 | 78 | Returns: 79 | DataFrame: A new DataFrame containing only rows that satisfy the condition. 80 | """ 81 | 82 | return self.where(condition) 83 | 84 | def with_column(self, name, expr) -> DataFrame: 85 | """Create a new DataFrame that has an additional column. 86 | 87 | Args: 88 | name (str): Name of the new column. 89 | expr (sql.expression): SQLAlchemy expression 90 | to compute the values of the new column. 91 | 92 | Returns: 93 | DataFrame: New DataFrame with an additional column. 94 | """ 95 | return DataFrame(select(self._stmt, expr.label(name)).cte(), self._engine) 96 | 97 | def with_column_renamed(self, old_name: str, new_name: str) -> DataFrame: 98 | """Create a new DataFrame with the given column renamed. 99 | 100 | Args: 101 | old_name (str): Name of the column to be renamed. 102 | new_name (str): New name of the column. 103 | 104 | Returns: 105 | DataFrame: New DataFrame with the given column renamed. 106 | """ 107 | cols = [ 108 | self[old_name].label(new_name) if col == old_name else self[col] 109 | for col in self.columns 110 | ] 111 | stmt = select(*cols) 112 | return DataFrame(stmt.cte(), self._engine) 113 | 114 | def drop(self, *args) -> DataFrame: 115 | """Returns a new DataFrame object with the specified columns removed. 116 | 117 | Args: 118 | *args: variable length argument list containing the column names to remove. 119 | 120 | Returns: 121 | A new DataFrame object with the specified columns removed. 122 | """ 123 | cols = [self[col] for col in self.columns if col not in args] 124 | stmt = select(*cols) 125 | return DataFrame(stmt.cte(), self._engine) 126 | 127 | def group_by(self, *args, **kwargs) -> DataFrame: 128 | """Create a new DataFrame that has grouped rows. 129 | 130 | Args: 131 | *args (Selectable): Columns to group by. 132 | **kwargs: 133 | agg (Dict[str, Tuple[Callable, str]]): 134 | Dictionary that maps from column name to 135 | (aggregation function, name of the aggregation). 136 | 137 | Returns: 138 | DataFrame: New DataFrame with grouped rows. 139 | """ 140 | grouped = None 141 | agg = kwargs.get("agg", None) 142 | if agg: 143 | agg_expr = [] 144 | for col_name, agg in agg.items(): 145 | agg_func, agg_name = agg 146 | agg_expr.append(agg_func(col_name).label(agg_name)) 147 | agg_expr = [*args, *agg_expr] 148 | grouped = select(*agg_expr).group_by(*args).cte() 149 | else: 150 | grouped = select(*args).group_by(*args).cte() 151 | return DataFrame(grouped, self._engine) 152 | 153 | def join(self, other: "DataFrame", expr, how: str = "inner") -> DataFrame: 154 | """ 155 | Join this DataFrame with another DataFrame. 156 | 157 | Args: 158 | other (DataFrame): The other DataFrame to join with. 159 | expr: The join expression. 160 | how (str): The type of join. Can be "inner", "left", "right", or "full". 161 | 162 | Returns: 163 | DataFrame: A new DataFrame with the result of the join. 164 | """ 165 | # Check that the join type is valid 166 | if how not in ["inner", "left", "right", "full"]: 167 | raise ValueError(f"Unsupported join type {how}.") 168 | 169 | # Perform the join operation 170 | if how == "left": 171 | stmt = select(join(self._stmt, other._stmt, expr, isouter=True)) 172 | elif how == "right": 173 | stmt = select(join(other._stmt, self._stmt, expr, isouter=True)) 174 | elif how == "full": 175 | stmt = select(join(self._stmt, other._stmt, expr, full=True)) 176 | else: 177 | stmt = select(join(self._stmt, other._stmt, expr)) 178 | 179 | # Return the result as a new DataFrame 180 | return DataFrame(stmt.cte(), self._engine) 181 | 182 | def show(self) -> None: 183 | """ 184 | Print the first 20 rows of the DataFrame. 185 | """ 186 | conn = self._engine.connect() 187 | q = select(self._stmt).limit(20) 188 | print(conn.execute(q).fetchall()) 189 | conn.close() 190 | 191 | def to_cte(self) -> sqlalchemy.sql.selectable.CTE: 192 | "Return the DataFrame as a SQLAlchemy cte" 193 | return self._stmt 194 | 195 | def materialize(self, name: str, as_: Literal["view", "table"]) -> None: 196 | """ 197 | Materialize the query as a table or view in the database. 198 | 199 | Args: 200 | name (str): The name of the table or view to create. 201 | as_ (Literal["view", "table"]): The type of object to create. 202 | 203 | Raises: 204 | ValueError: If an unsupported materialization type is specified. 205 | 206 | """ 207 | # Convert the statement to a SELECT statement 208 | self._stmt = select(self._stmt) 209 | 210 | if as_ == "view": 211 | materialization = CreateViewAs(name, self._stmt) 212 | elif as_ == "table": 213 | materialization = CreateTableAs(name, self._stmt) 214 | else: 215 | # Raise an error if an unsupported materialization type is specified 216 | raise ValueError(f"Unsupported materialization type: {as_}") 217 | 218 | # Execute the materialization query 219 | q = materialization.compile(bind=self._engine) 220 | conn = self._engine.connect() 221 | conn.execute(q) 222 | conn.commit() 223 | conn.close() 224 | 225 | def collect(self) -> List[dict]: 226 | """ 227 | Retrieve all the data in the dataframe as a list. 228 | 229 | Returns: 230 | List[dict]: A list of dictionaries, 231 | where each dictionary represents a row in the dataframe. 232 | """ 233 | conn = self._engine.connect() 234 | result = conn.execute(select(self._stmt)).fetchall() 235 | conn.close() 236 | return result 237 | 238 | def union(self, other: "DataFrame") -> DataFrame: 239 | """ 240 | Return a new DataFrame that is the union 241 | of this DataFrame and another DataFrame. 242 | 243 | Args: 244 | other (DataFrame): The other DataFrame to union with. 245 | 246 | Returns: 247 | DataFrame: A new DataFrame that is the union 248 | of this DataFrame and another DataFrame. 249 | """ 250 | # Check if the two dataframes have the same columns 251 | # if not add a column with null values for missing column 252 | missing_columns = set(self.columns) - set(other.columns) 253 | if missing_columns: 254 | for col in missing_columns: 255 | other = other.with_column(col, sqlalchemy.sql.expression.null()) 256 | 257 | missing_columns = set(other.columns) - set(self.columns) 258 | if missing_columns: 259 | for col in missing_columns: 260 | self = self.with_column(col, sqlalchemy.sql.expression.null()) 261 | 262 | # Construct the union statement 263 | union_stmt = union_all(select(self._stmt), select(other._stmt)).cte() 264 | 265 | # Return the result as a new DataFrame 266 | return DataFrame(union_stmt, self._engine) 267 | 268 | def distinct(self) -> DataFrame: 269 | """Create a new DataFrame with only distinct rows. 270 | Returns: 271 | DataFrame: New DataFrame with only distinct rows based on the columns specified. 272 | """ 273 | stmt = select(self._stmt).distinct() 274 | 275 | return DataFrame(stmt.cte(), self._engine) 276 | -------------------------------------------------------------------------------- /pydwt/sql/materializations.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.compiler import compiles 2 | from sqlalchemy.sql.expression import ClauseElement, Executable 3 | from sqlalchemy.sql.selectable import Selectable 4 | from typing import Any 5 | 6 | 7 | class CreateTableAs(Executable, ClauseElement): 8 | """_summary_ 9 | :param Executable: _description_ 10 | :type Executable: _type_ 11 | :param ClauseElement: _description_ 12 | :type ClauseElement: _type_ 13 | """ 14 | 15 | inherit_cache = True 16 | 17 | def __init__(self, name: str, select_query: Selectable): 18 | self.name = name 19 | self.select_query = select_query 20 | 21 | 22 | @compiles(CreateTableAs) 23 | def visit_create_table_as(element: Any, compiler: Any, **kw: str) -> str: 24 | """_summary_ 25 | :param element: _description_ 26 | :type element: Any 27 | :param compiler: _description_ 28 | :type compiler: Any 29 | :return: _description_ 30 | :rtype: str 31 | """ 32 | return """ 33 | DROP TABLE IF EXISTS {0}; 34 | CREATE TABLE {0} 35 | AS 36 | {1} 37 | """.format( 38 | element.name, 39 | compiler.process(element.select_query, literal_binds=True), 40 | ) 41 | 42 | 43 | class CreateViewAs(Executable, ClauseElement): 44 | """_summary_ 45 | :param Executable: _description_ 46 | :type Executable: _type_ 47 | :param ClauseElement: _description_ 48 | :type ClauseElement: _type_ 49 | """ 50 | 51 | inherit_cache = True 52 | 53 | def __init__(self, name: str, select_query: Selectable): 54 | self.name = name 55 | self.select_query = select_query 56 | 57 | 58 | @compiles(CreateViewAs) 59 | def visit_create_view_as(element: Any, compiler: Any, **kw: str) -> str: 60 | """_summary_ 61 | :param element: _description_ 62 | :type element: Any 63 | :param compiler: _description_ 64 | :type compiler: Any 65 | :return: _description_ 66 | :rtype: str 67 | """ 68 | return """ 69 | CREATE OR REPLACE VIEW {} 70 | AS 71 | {} 72 | """.format( 73 | element.name, 74 | compiler.process(element.select_query, literal_binds=True), 75 | ) 76 | -------------------------------------------------------------------------------- /pydwt/sql/session.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from sqlalchemy import select, Table, MetaData 3 | from pydwt.sql.dataframe import DataFrame 4 | 5 | 6 | class Session: 7 | def __init__(self, engine, schema: str = None): 8 | """Initialize a new Session object. 9 | 10 | Args: 11 | engine: A SQLAlchemy engine object. 12 | schema (str): The schema of the database. 13 | """ 14 | self._engine = engine 15 | if schema: 16 | self._metadata = MetaData(schema=schema) 17 | else: 18 | self._metadata = MetaData() 19 | 20 | def table(self, name: str) -> DataFrame: 21 | """Create a new DataFrame object from the given table name. 22 | 23 | Args: 24 | name (str): The name of the table. 25 | 26 | Returns: 27 | DataFrame: A new DataFrame object. 28 | """ 29 | t = Table(name, self._metadata, autoload_with=self._engine) 30 | base = select(t).cte() 31 | return DataFrame(base, self._engine) 32 | 33 | def create_dataframe(self, stmt: Any) -> DataFrame: 34 | """Create a DataFrame from the seletable. 35 | 36 | Args: 37 | stmt (Selectable): SQLAlchemy selectable 38 | 39 | Returns: 40 | DataFrame: new DataFrame with the given Selectable as base. 41 | """ 42 | return DataFrame(stmt, self._engine) 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pydwt" 3 | version = "0.2.0a0" 4 | description = "" 5 | authors = ["gonza "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8.1" 10 | networkx = "^3.0" 11 | matplotlib = "^3.6.3" 12 | sqlalchemy = "^2.0.1" 13 | typer = "^0.7.0" 14 | pyyaml = "^6.0" 15 | dependency-injector = "^4.41.0" 16 | 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | pytest = "^7.2.1" 20 | black = "^23.1.0" 21 | pytest-cov = "^4.0.0" 22 | flake8 = "^6.0.0" 23 | 24 | [build-system] 25 | requires = ["poetry-core"] 26 | build-backend = "poetry.core.masonry.api" 27 | 28 | [tool.poetry.scripts] 29 | pydwt = "pydwt.app:app" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mg30/pydwt/28b82aaa3f77f29de29a659c04b388afceb39aaa/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from typer.testing import CliRunner 2 | from pydwt.app import app, container 3 | import os 4 | import shutil 5 | import pytest 6 | from pydwt.context.connection import Connection 7 | 8 | args = {"url": "sqlite:///:memory:", "echo": True} 9 | 10 | container.database_client.override(Connection(args)) 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def setup(): 15 | yield None 16 | # clean up 17 | shutil.rmtree("my_project") 18 | os.remove("settings.yml") 19 | 20 | 21 | def test_new(setup): 22 | runner = CliRunner() 23 | result = runner.invoke(app, ["new", "my_project"]) 24 | assert result.exit_code == 0 25 | 26 | 27 | def test_export_dag(setup): 28 | runner = CliRunner() 29 | result = runner.invoke(app, ["export-dag"]) 30 | assert result.exit_code == 0 31 | 32 | 33 | def test_connection_testing(setup): 34 | runner = CliRunner() 35 | result = runner.invoke(app, ["test-connection"]) 36 | assert result.exit_code == 0 -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydwt.context.connection import Connection 3 | 4 | 5 | def test_connection(): 6 | args = {"url": "sqlite:///:memory:", "echo": True} 7 | conn = Connection(args) 8 | assert conn 9 | 10 | def test_connection_get_engine(): 11 | args = {"url": "sqlite:///:memory:", "echo": True} 12 | conn = Connection(args) 13 | engine = conn.get_engine() 14 | assert engine.connect() 15 | -------------------------------------------------------------------------------- /tests/test_dags.py: -------------------------------------------------------------------------------- 1 | from pydwt.core.task import Task 2 | from pydwt.core.dag import Dag 3 | import pytest 4 | import unittest.mock 5 | 6 | from pydwt.core.containers import Container 7 | from pydwt.core.enums import Status 8 | 9 | container = Container() 10 | container.database_client.override(unittest.mock.Mock()) 11 | container.wire(modules=["pydwt.core.task"]) 12 | 13 | 14 | @pytest.fixture 15 | def dag(): 16 | def fake_task_one(): 17 | pass 18 | 19 | def fake_task_two(): 20 | pass 21 | 22 | task1 = Task(retry=2) 23 | task1(fake_task_one) 24 | 25 | task2 = Task(depends_on=[fake_task_one]) 26 | task2(fake_task_two) 27 | dag = Dag() 28 | dag.tasks = [task1, task2] 29 | dag.build_dag() 30 | return dag 31 | 32 | 33 | def test_dag_init(): 34 | def fake_task_one(): 35 | pass 36 | 37 | def fake_task_two(): 38 | pass 39 | 40 | task1 = Task(retry=2) 41 | task1(fake_task_one) 42 | 43 | task2 = Task(depends_on=[fake_task_one]) 44 | task2(fake_task_two) 45 | dag = Dag() 46 | dag.tasks = [task1, task2] 47 | assert dag 48 | 49 | 50 | def test_build_dag_nodes(dag): 51 | """Test that the dag is built correctly.""" 52 | assert list(dag.graph.nodes()) == [0, 1, "s"] 53 | 54 | 55 | def test_build_dag_edges(dag): 56 | """Test that the dag is built correctly.""" 57 | assert list(dag.graph.edges()) == [(0, 1), ("s", 0)] 58 | 59 | 60 | def test_build_dag_node_name(dag): 61 | """Test that the dag is built correctly.""" 62 | assert dag.graph.nodes[0]["name"] == "tests.test_dags.fake_task_one" 63 | 64 | 65 | def test_build_level(dag): 66 | """Test that the levels are assigned correctly.""" 67 | levels = dag.build_level() 68 | assert levels == {0: ["s"], 1: [0], 2: [1]} 69 | 70 | 71 | def test_dag_check_parents_status_error(): 72 | def fake_task_one(): 73 | raise ValueError("fake error") 74 | 75 | def fake_task_two(): 76 | pass 77 | 78 | task1 = Task(retry=2) 79 | task1(fake_task_one) 80 | 81 | task2 = Task(depends_on=[fake_task_one]) 82 | task2(fake_task_two) 83 | dag = Dag() 84 | dag.tasks = [task1, task2] 85 | dag.build_dag() 86 | task1.run() 87 | task2.run() 88 | assert dag.check_parents_status(task2) == Status.ERROR 89 | 90 | 91 | def test_dag_check_parents_status_success(): 92 | def fake_task_one(): 93 | pass 94 | 95 | def fake_task_two(): 96 | pass 97 | 98 | task1 = Task(retry=2) 99 | task1(fake_task_one) 100 | 101 | task2 = Task(depends_on=[fake_task_one]) 102 | task2(fake_task_two) 103 | dag = Dag() 104 | dag.tasks = [task1, task2] 105 | dag.build_dag() 106 | task1.run() 107 | task2.run() 108 | assert dag.check_parents_status(task2) == Status.SUCCESS 109 | 110 | 111 | def test_dag_build_level_task_name(): 112 | def fake_task_one(): 113 | pass 114 | 115 | def fake_task_two(): 116 | pass 117 | 118 | def fake_task_three(): 119 | pass 120 | 121 | task1 = Task(retry=2) 122 | task1(fake_task_one) 123 | 124 | task2 = Task(depends_on=[fake_task_one]) 125 | task2(fake_task_two) 126 | 127 | task3 = Task(depends_on=[fake_task_one]) 128 | task3(fake_task_three) 129 | 130 | dag = Dag() 131 | dag.tasks = [task1, task2, task3] 132 | dag.build_dag() 133 | levels = dag.build_level(target="tests.test_dags.fake_task_three") 134 | assert levels == {0: ["s"], 1: [0], 2: [2]} 135 | 136 | 137 | def test_filter_dag(): 138 | def fake_task_one(): 139 | pass 140 | 141 | def fake_task_two(): 142 | pass 143 | 144 | def fake_task_three(): 145 | pass 146 | 147 | def fake_task_four(): 148 | pass 149 | 150 | def fake_task_five(): 151 | pass 152 | 153 | task1 = Task(retry=2) 154 | task1(fake_task_one) 155 | 156 | task2 = Task(depends_on=[fake_task_one]) 157 | task2(fake_task_two) 158 | 159 | task3 = Task(depends_on=[fake_task_one]) 160 | task3(fake_task_three) 161 | 162 | task4 = Task(depends_on=[fake_task_two, fake_task_three]) 163 | task4(fake_task_four) 164 | 165 | task5 = Task(depends_on=[fake_task_two]) 166 | task5(fake_task_five) 167 | 168 | dag = Dag() 169 | dag.tasks = [task1, task2, task3, task4, task5] 170 | dag.build_dag() 171 | dag.filter_dag(task_name="tests.test_dags.fake_task_four") 172 | taks_name = [task.name for task in dag.tasks] 173 | assert taks_name == [ 174 | "tests.test_dags.fake_task_one", 175 | "tests.test_dags.fake_task_two", 176 | "tests.test_dags.fake_task_three", 177 | "tests.test_dags.fake_task_four", 178 | ] 179 | -------------------------------------------------------------------------------- /tests/test_dataframe.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | create_engine, 3 | Table, 4 | Column, 5 | Integer, 6 | String, 7 | literal_column, 8 | MetaData, 9 | func, 10 | ForeignKey, 11 | column, 12 | text, 13 | ) 14 | from pydwt.sql.session import Session 15 | import pytest 16 | import sqlalchemy 17 | from pydwt.sql.dataframe import DataFrame 18 | 19 | 20 | # Define a fixture that creates a fake table using SQLite 21 | @pytest.fixture(scope="session") 22 | def session(): 23 | engine = create_engine("sqlite:///:memory:", echo=True) 24 | metadata = MetaData() 25 | 26 | t = Table( 27 | "users", 28 | metadata, 29 | Column("user_id", Integer, primary_key=True), 30 | Column("name", String), 31 | Column("age", Integer), 32 | ) 33 | 34 | products = Table( 35 | "products", 36 | metadata, 37 | Column("id", Integer, primary_key=True), 38 | Column("name", String), 39 | Column("user_id", Integer, ForeignKey("users.user_id")), 40 | ) 41 | 42 | metadata.create_all(bind=engine) 43 | 44 | # Insert some data into the table 45 | conn = engine.connect() 46 | conn.execute( 47 | t.insert(), 48 | [ 49 | {"user_id": 1, "name": "Alice", "age": 25}, 50 | {"user_id": 2, "name": "Bob", "age": 30}, 51 | {"user_id": 3, "name": "Charlie", "age": 35}, 52 | {"user_id": 4, "name": "David", "age": 40}, 53 | {"user_id": 5, "name": "Eve", "age": 45}, 54 | ], 55 | ) 56 | conn.execute( 57 | products.insert(), 58 | [ 59 | {"name": "Product A", "user_id": 1}, 60 | {"name": "Product B", "user_id": 2}, 61 | {"name": "Product C", "user_id": 2}, 62 | {"name": "Product D", "user_id": 3}, 63 | {"name": "Product D", "user_id": 3}, 64 | {"name": "Product E", "user_id": 4}, 65 | {"name": "Product F", "user_id": 5}, 66 | ], 67 | ) 68 | conn.commit() 69 | conn.close() 70 | 71 | session = Session(engine) 72 | 73 | yield session 74 | 75 | metadata.drop_all(bind=engine) 76 | engine.dispose() 77 | 78 | 79 | def test_dataframe_select(session): 80 | df = session.table("users") 81 | df2 = df.select(df.name, df.age) 82 | 83 | assert df2.columns == ["name", "age"] 84 | 85 | 86 | def test_dataframe_select_string(session): 87 | df = session.table("users") 88 | df2 = df.select("name", "age") 89 | 90 | assert df2.columns == ["name", "age"] 91 | 92 | 93 | def test_dataframe_select_column(session): 94 | df = session.table("users") 95 | df2 = df.select(column("name"), column("age")) 96 | 97 | assert df2.columns == ["name", "age"] 98 | 99 | 100 | def test_dataframe_where(session): 101 | df = session.table("users") 102 | df2 = df.where(literal_column("age") > 30) 103 | 104 | assert len(df2) == 3 105 | 106 | 107 | def test_dataframe_filter(session): 108 | df = session.table("users") 109 | df2 = df.where(literal_column("age") > 30) 110 | 111 | assert len(df2) == 3 112 | 113 | 114 | def test_dataframe_filter_string(session): 115 | df = session.table("users") 116 | df2 = df.filter("age > 30") 117 | 118 | assert len(df2) == 3 119 | 120 | 121 | def test_dataframe_where_string(session): 122 | df = session.table("users") 123 | df2 = df.where("age > 30") 124 | 125 | assert len(df2) == 3 126 | 127 | 128 | def test_dataframe_with_column(session): 129 | df = session.table("users") 130 | df2 = df.with_column("age2", literal_column("age") * 2) 131 | 132 | assert df2.columns == ["user_id", "name", "age", "age2"] 133 | 134 | 135 | def test_dataframe_group_by(session): 136 | df = session.table("users") 137 | df2 = df.group_by(df.age, agg={"user_id": (func.min, "min_id")}) 138 | assert df2.columns == ["age", "min_id"] 139 | 140 | 141 | def test_dataframe_with_column_renamed(session): 142 | df1 = session.table("users") 143 | df1 = df1.with_column_renamed("name", "user_name") 144 | 145 | assert df1.columns == ["user_id", "user_name", "age"] 146 | 147 | 148 | def test_dataframe_join(session): 149 | df1 = session.table("users") 150 | df1 = df1.with_column_renamed("name", "user_name") 151 | df2 = session.table("products") 152 | df2 = df2.with_column_renamed("name", "product_name") 153 | df2 = df2.with_column_renamed("user_id", "user_id_") 154 | df3 = df1.join(df2, (df1.user_id == df2.user_id_)) 155 | 156 | assert df3.select(df3.age) 157 | 158 | 159 | def test_dataframe_union(session): 160 | df1 = session.table("users") 161 | df1 = df1.with_column("other_col", literal_column("age") * 2) 162 | 163 | df2 = session.table("users") 164 | 165 | df3 = df1.union(df2) 166 | 167 | assert "other_col" in df3.columns 168 | assert df3.collect() 169 | 170 | 171 | def test_dataframe_drop(session): 172 | df1 = session.table("users") 173 | df1 = df1.drop("age") 174 | 175 | assert "age" not in df1.columns 176 | assert df1.collect() 177 | 178 | 179 | def test_dataframe_to_cte(session): 180 | df1 = session.table("users") 181 | stmt = df1.to_cte() 182 | 183 | assert type(stmt) == sqlalchemy.sql.selectable.CTE 184 | 185 | 186 | def test_create_dataframe(session): 187 | df1 = session.table("users") 188 | stmt = df1.to_cte() 189 | df2 = session.create_dataframe(stmt) 190 | assert type(df2) == DataFrame 191 | 192 | 193 | def test_distinct_one_col(session): 194 | df = session.table("products") 195 | df = df.select("user_id").distinct() 196 | values = df.collect() 197 | assert values == [(1,), (2,), (3,), (4,), (5,)] 198 | 199 | 200 | def test_distinct_multi_col(session): 201 | df = session.table("products") 202 | df = df.select(df.user_id, df.name).distinct() 203 | values = df.collect() 204 | assert values == [ 205 | (1, "Product A"), 206 | (2, "Product B"), 207 | (2, "Product C"), 208 | (3, "Product D"), 209 | (4, "Product E"), 210 | (5, "Product F"), 211 | ] 212 | -------------------------------------------------------------------------------- /tests/test_executors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pydwt.core.containers import Container 3 | from pydwt.core.task import Task 4 | from pydwt.core.dag import Dag 5 | from pydwt.core.executors import ThreadExecutor 6 | import pytest 7 | 8 | 9 | container = Container() 10 | container.wire(modules=["pydwt.core.task"]) 11 | 12 | 13 | @pytest.fixture 14 | def fake_task_one(): 15 | def inner_func_one(): 16 | pass 17 | 18 | return inner_func_one 19 | 20 | 21 | @pytest.fixture 22 | def fake_task_two(): 23 | def inner_func_two(): 24 | pass 25 | 26 | return inner_func_two 27 | 28 | 29 | @pytest.fixture 30 | def fake_task_three(): 31 | def inner_func_third(): 32 | raise ValueError("fake error") 33 | 34 | return inner_func_third 35 | 36 | 37 | def test_thread_executor_runs_all_tasks(fake_task_one, fake_task_two): 38 | task = Task(retry=2) 39 | task(fake_task_one) 40 | 41 | task2 = Task() 42 | task2(fake_task_two) 43 | tasks = [task, task2] 44 | dag = Dag() 45 | dag.tasks = tasks 46 | dag.build_dag() 47 | executor = ThreadExecutor(dag) 48 | executor.tasks = tasks 49 | executor.run() 50 | 51 | assert task2._count_call == 1 52 | assert task._count_call == 1 53 | 54 | 55 | def test_thread_executor_no_when_parent_is_error(fake_task_one, fake_task_two, fake_task_three): 56 | task = Task(retry=2) 57 | task(fake_task_one) 58 | 59 | task3 = Task() 60 | task3(fake_task_three) 61 | 62 | task2 = Task(depends_on=[fake_task_one, fake_task_three]) 63 | task2(fake_task_two) 64 | 65 | tasks = [task2, task3, task] 66 | 67 | dag = Dag() 68 | dag.tasks = tasks 69 | dag.build_dag() 70 | 71 | executor = ThreadExecutor(dag) 72 | executor.tasks = tasks 73 | 74 | executor.run() 75 | 76 | assert task2._count_call == 0 77 | 78 | -------------------------------------------------------------------------------- /tests/test_project.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import pytest 4 | from pydwt.core.project import Project 5 | import os 6 | 7 | 8 | 9 | @pytest.fixture(scope="session") 10 | def setup(): 11 | project_name = "test_project" 12 | models_folder = "models" 13 | dags_folder = "dags" 14 | proj = Project(name=project_name, models_folder=models_folder, dags_folder=dags_folder, workflow=None) 15 | proj.new("test_project") 16 | yield proj 17 | # Clean up the test project directory 18 | shutil.rmtree(project_name) 19 | os.remove("settings.yml") 20 | 21 | def test_project_dir_exist(setup): 22 | # Test that the project directories were created 23 | assert os.path.exists("test_project") 24 | 25 | def test_project_dir_models_exists(setup): 26 | # Test that the project directories were created 27 | assert os.path.exists(os.path.join("test_project", "models")) 28 | 29 | 30 | def test_project_dir_dags_exists(setup): 31 | # Test that the project directories were created 32 | assert os.path.exists(os.path.join("test_project", "dags")) 33 | 34 | def test_project_settings_exists(setup): 35 | # Test that the project directories were created 36 | assert os.path.exists("settings.yml") 37 | assert os.path.exists(os.path.join("test_project", "models", "example.py")) 38 | 39 | def test_project_example_exists(setup): 40 | # Test that the project directories were created 41 | assert os.path.exists("settings.yml") 42 | assert os.path.exists(os.path.join("test_project", "models", "example.py")) -------------------------------------------------------------------------------- /tests/test_schedule.py: -------------------------------------------------------------------------------- 1 | from pydwt.core.schedule import * 2 | import datetime 3 | import pytest 4 | from calendar import Calendar 5 | 6 | 7 | @pytest.fixture 8 | def calendar(): 9 | return Calendar() 10 | 11 | 12 | def test_daily(): 13 | daily = Daily() 14 | assert daily.is_scheduled() == True 15 | 16 | 17 | def test_is_sechduled_weekly(calendar): 18 | date = datetime.datetime.now() 19 | weeks_of_month = calendar.monthdatescalendar(date.year, date.month) 20 | week = weeks_of_month[0] 21 | for date in week: 22 | weekday = Weekly(weekday=date.weekday()) 23 | assert weekday.is_scheduled(date) 24 | 25 | 26 | def test_is_not_sechduled_weekly(calendar): 27 | date = datetime.datetime.now() 28 | weeks_of_month = calendar.monthdatescalendar(date.year, date.month) 29 | week = weeks_of_month[0] 30 | for date in week: 31 | weekday = Weekly(weekday=date.weekday() + 1) 32 | assert not weekday.is_scheduled(date) 33 | 34 | 35 | def test_is_scheduled_monthly(calendar): 36 | date = datetime.datetime.now() 37 | weeks_of_month = calendar.monthdatescalendar(date.year, date.month) 38 | first_week = weeks_of_month[0] 39 | first_week = [ 40 | d for d in first_week if d.year == date.year and d.month == date.month 41 | ] 42 | for date in first_week: 43 | schedule = Monthly(weekday=date.weekday()) 44 | datet = datetime.datetime(date.year, date.month, date.day) 45 | assert schedule.is_scheduled(datet) 46 | 47 | 48 | def test_is_not_scheduled_monthly(calendar): 49 | date = datetime.datetime.now() 50 | weeks_of_month = calendar.monthdatescalendar(date.year, date.month) 51 | not_first_week = weeks_of_month[2] 52 | not_first_week = [ 53 | d for d in not_first_week if d.year == date.year and d.month == date.month 54 | ] 55 | for date in not_first_week: 56 | schedule = Monthly(weekday=date.weekday()) 57 | datet = datetime.datetime(date.year, date.month, date.day) 58 | assert not schedule.is_scheduled(datet) 59 | 60 | 61 | def test_is_schedule_last_open_day_in_month(): 62 | dates = [ 63 | datetime.datetime(2023, 2, 28), 64 | datetime.datetime(2023, 1, 31), 65 | datetime.datetime(2022, 12, 30), 66 | datetime.datetime(2022, 7, 29) 67 | ] 68 | s = MonthlyLastOpenDayInMonth() 69 | for d in dates: 70 | assert s.is_scheduled(d) 71 | 72 | 73 | def test_is_not_schedule_last_open_day_in_month(): 74 | dates = [datetime.datetime(2022, 12, 31),datetime.datetime(2022, 7, 31),] 75 | s = MonthlyLastOpenDayInMonth() 76 | for d in dates: 77 | assert not s.is_scheduled(d) 78 | -------------------------------------------------------------------------------- /tests/test_task.py: -------------------------------------------------------------------------------- 1 | from pydwt.core.task import Task 2 | import pytest 3 | from unittest import mock 4 | from pydwt.core.containers import Container 5 | from pydwt.core.schedule import Monthly 6 | from pydwt.core.enums import Status 7 | 8 | 9 | container = Container() 10 | container.database_client.override(mock.Mock()) 11 | container.wire(modules=["pydwt.core.task"]) 12 | 13 | @pytest.fixture 14 | def fake_task_one(): 15 | def inner_func(): 16 | pass 17 | 18 | return inner_func 19 | 20 | 21 | @pytest.fixture 22 | def fake_task_two(): 23 | def inner_func_bis(): 24 | pass 25 | 26 | return inner_func_bis 27 | 28 | @pytest.fixture 29 | def fake_task_three(): 30 | def inner_func_third(): 31 | raise ValueError("fake error") 32 | 33 | return inner_func_third 34 | 35 | 36 | 37 | def test_task_not_eq(fake_task_one, fake_task_two): 38 | 39 | 40 | task = Task(retry=2) 41 | task(fake_task_one) 42 | 43 | task2 = Task() 44 | task2(fake_task_two) 45 | 46 | assert task != task2 47 | 48 | def test_task_no_retry_run_once(fake_task_one): 49 | task = Task() 50 | task(fake_task_one) 51 | task.run() 52 | assert task._count_call == 1 53 | 54 | 55 | def test_task_not_scheduled(fake_task_one): 56 | task = Task(runs_on=Monthly()) 57 | task(fake_task_one) 58 | task.run() 59 | assert task._count_call == 0 60 | 61 | def test_task_is_scheduled(fake_task_one): 62 | task = Task() 63 | task(fake_task_one) 64 | task.run() 65 | assert task._count_call == 1 66 | 67 | def test_task_eq(fake_task_one): 68 | task = Task(retry=2) 69 | task(fake_task_one) 70 | 71 | task2 = Task(retry=2) 72 | task2(fake_task_one) 73 | 74 | assert task == task2 75 | 76 | 77 | def test_task_status_error(fake_task_three): 78 | task = Task() 79 | task(fake_task_three) 80 | task.run() 81 | 82 | assert task.status == Status.ERROR 83 | 84 | 85 | def test_task_status_success(fake_task_one): 86 | task = Task() 87 | task(fake_task_one) 88 | task.run() 89 | 90 | assert task.status == Status.SUCCESS --------------------------------------------------------------------------------