├── .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
--------------------------------------------------------------------------------