├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── taskiq_pipelines ├── __init__.py ├── abc.py ├── constants.py ├── exceptions.py ├── middleware.py ├── pipeliner.py ├── py.typed └── steps │ ├── __init__.py │ ├── filter.py │ ├── mapper.py │ └── sequential.py └── tests ├── conftest.py └── test_steps.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release python package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: "3.9" 17 | - name: Install deps 18 | uses: knowsuchagency/poetry-install@v1 19 | env: 20 | POETRY_VIRTUALENVS_CREATE: false 21 | - name: Set verison 22 | run: poetry version "${{ github.ref_name }}" 23 | - name: Release package 24 | env: 25 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 26 | run: poetry publish --build 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing taskiq-pipelines 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | lint: 7 | strategy: 8 | matrix: 9 | cmd: 10 | - black 11 | - ruff 12 | - mypy 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install poetry 17 | run: pipx install poetry 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.11" 22 | cache: "poetry" 23 | - name: Install deps 24 | run: poetry install --all-extras 25 | - name: Run lint check 26 | run: poetry run pre-commit run -a ${{ matrix.cmd }} 27 | pytest: 28 | strategy: 29 | matrix: 30 | py_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 31 | runs-on: "ubuntu-latest" 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Set up Python 35 | uses: actions/setup-python@v2 36 | with: 37 | python-version: "${{ matrix.py_version }}" 38 | - name: Update pip 39 | run: python -m pip install -U pip 40 | - name: Install poetry 41 | run: python -m pip install poetry 42 | - name: Install deps 43 | run: poetry install 44 | env: 45 | POETRY_VIRTUALENVS_CREATE: false 46 | - name: Run pytest check 47 | run: poetry run pytest -vv -n auto --cov="taskiq_pipelines" . 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | coverage.* 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # PyCharm 157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 159 | # and can be added to the global gitignore or merged into this file. For a more nuclear 160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 161 | .idea/ 162 | .vscode/ 163 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.4.0 6 | hooks: 7 | - id: check-ast 8 | - id: trailing-whitespace 9 | - id: check-toml 10 | - id: end-of-file-fixer 11 | 12 | - repo: https://github.com/asottile/add-trailing-comma 13 | rev: v2.1.0 14 | hooks: 15 | - id: add-trailing-comma 16 | 17 | - repo: local 18 | hooks: 19 | - id: black 20 | name: Format with Black 21 | entry: poetry run black 22 | language: system 23 | types: [python] 24 | 25 | - id: ruff 26 | name: Run ruff lints 27 | entry: poetry run ruff 28 | language: system 29 | pass_filenames: false 30 | types: [python] 31 | args: 32 | - "check" 33 | - "--fix" 34 | - "." 35 | 36 | - id: mypy 37 | name: Validate types with MyPy 38 | entry: poetry run mypy 39 | language: system 40 | types: [python] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pavel Kirilin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pipelines for taskiq 2 | 3 | Taskiq pipelines is a `fire-and-forget` at its limit. 4 | 5 | Imagine you have a really tough functions and you want 6 | to call them sequentially one after one, but you don't want to wait for them 7 | to complete. taskiq-pipeline solves this for you. 8 | 9 | ## Installation 10 | 11 | 12 | You can install it from pypi: 13 | ``` 14 | pip install taskiq-pipelines 15 | ``` 16 | 17 | After you installed it you need to add our super clever middleware 18 | to your broker. 19 | 20 | This middleware actually decides what to do next, after current step 21 | is completed. 22 | 23 | ```python 24 | from taskiq_pipelines.middleware import PipelineMiddleware 25 | 26 | my_super_broker = ... 27 | 28 | 29 | my_super_broker.add_middlewares(PipelineMiddleware()) 30 | ``` 31 | 32 | Also we have to admit that your broker MUST use result_backend that 33 | can be read by all your workers. Pipelines work with inmemorybroker, 34 | feel free to use it in local development. 35 | 36 | 37 | ### Example 38 | 39 | For this example I'm going to use one single script file. 40 | 41 | ```python 42 | import asyncio 43 | from typing import Any, List 44 | from taskiq.brokers.inmemory_broker import InMemoryBroker 45 | from taskiq_pipelines import PipelineMiddleware, Pipeline 46 | 47 | broker = InMemoryBroker() 48 | broker.add_middlewares(PipelineMiddleware()) 49 | 50 | 51 | @broker.task 52 | def add_one(value: int) -> int: 53 | return value + 1 54 | 55 | 56 | @broker.task 57 | def repeat(value: Any, reps: int) -> List[Any]: 58 | return [value] * reps 59 | 60 | 61 | @broker.task 62 | def check(value: int) -> bool: 63 | return value >= 0 64 | 65 | 66 | async def main(): 67 | pipe = ( 68 | Pipeline( 69 | broker, 70 | add_one, # First of all we call add_one function. 71 | ) 72 | # 2 73 | .call_next(repeat, reps=4) # Here we repeat our value 4 times 74 | # [2, 2, 2, 2] 75 | .map(add_one) # Here we execute given function for each value. 76 | # [3, 3, 3, 3] 77 | .filter(check) # Here we filter some values. 78 | # But sice our filter filters out all numbers less than zero, 79 | # our value won't change. 80 | # [3, 3, 3, 3] 81 | ) 82 | task = await pipe.kiq(1) 83 | result = await task.wait_result() 84 | print("Calculated value:", result.return_value) 85 | 86 | 87 | if __name__ == "__main__": 88 | asyncio.run(main()) 89 | 90 | ``` 91 | 92 | If you run this example, it prints this: 93 | ```bash 94 | $ python script.py 95 | Calculated value: [3, 3, 3, 3] 96 | ``` 97 | 98 | Let's talk about this example. 99 | Two notable things here: 100 | 1. We must add PipelineMiddleware in the list of our middlewares. 101 | 2. We can use only tasks as functions we wan to execute in pipeline. 102 | If you want to execute ordinary python function - you must wrap it in task. 103 | 104 | Pipeline itself is just a convinient wrapper over list of steps. 105 | Constructed pipeline has the same semantics as the ordinary task, and you can add steps 106 | manually. But all steps of the pipeline must implement `taskiq_pipelines.abc.AbstractStep` class. 107 | 108 | Pipelines can be serialized to strings with `dumps` method, and you can load them back with `Pipeline.loads` method. So you can share pipelines you want to execute as simple strings. 109 | 110 | Pipeline assign `task_id` for each task when you call `kiq`, and executes every step with pre-calculated `task_id`, 111 | so you know all task ids after you call kiq method. 112 | 113 | 114 | ## How does it work? 115 | 116 | After you call `kiq` method of the pipeline it pre-calculates 117 | all task_ids, serializes itself and adds serialized string to 118 | the labels of the first task in the chain. 119 | 120 | All the magic happens in the middleware. 121 | After task is executed and result is saved, you can easily deserialize pipeline 122 | back and calculate pipeline's next move. And that's the trick. 123 | You can get more information from the source code of each pipeline step. 124 | 125 | # Available steps 126 | 127 | We have a few steps available for chaining calls: 128 | 1. Sequential 129 | 2. Mapper 130 | 3. Filter 131 | 132 | ### Sequential steps 133 | 134 | This type of step is just an ordinary call of the function. 135 | If you haven't specified `param_name` argument, then the result 136 | of the previous step will be passed as the first argument of the function. 137 | If you did specify the `param_name` argument, then the result of the previous 138 | step can be found in key word arguments with the param name you specified. 139 | 140 | You can add sequential steps with `.call_next` method of the pipeline. 141 | 142 | If you don't want to pass the result of the previous step to the next one, 143 | you can use `.call_after` method of the pipeline. 144 | 145 | ### Mapper step 146 | 147 | This step runs specified task for each item of the previous task's result spawning 148 | multiple tasks. 149 | But I have to admit, that the result of the previous task must be iterable. 150 | Otherwise it will mark the pipeline as failed. 151 | 152 | After the execution you'll have mapped list. 153 | You can add mappers by calling `.map` method of the pipeline. 154 | 155 | ### Filter step 156 | 157 | This step runs specified task for each item of the previous task's result. 158 | But I have to admit, that the result of the previous task must be iterable. 159 | Otherwise it will mark the pipeline as failed. 160 | 161 | If called tasks returned `True` for some element, this element will be added in the final list. 162 | 163 | After the execution you'll get a list with filtered results. 164 | You can add filters by calling `.filter` method of the pipeline. 165 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.7.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | groups = ["main"] 10 | files = [ 11 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 12 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 13 | ] 14 | 15 | [[package]] 16 | name = "anyio" 17 | version = "4.8.0" 18 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 19 | optional = false 20 | python-versions = ">=3.9" 21 | groups = ["main", "dev"] 22 | files = [ 23 | {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, 24 | {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, 25 | ] 26 | 27 | [package.dependencies] 28 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 29 | idna = ">=2.8" 30 | sniffio = ">=1.1" 31 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 32 | 33 | [package.extras] 34 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 35 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] 36 | trio = ["trio (>=0.26.1)"] 37 | 38 | [[package]] 39 | name = "black" 40 | version = "25.1.0" 41 | description = "The uncompromising code formatter." 42 | optional = false 43 | python-versions = ">=3.9" 44 | groups = ["dev"] 45 | files = [ 46 | {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, 47 | {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, 48 | {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, 49 | {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, 50 | {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, 51 | {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, 52 | {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, 53 | {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, 54 | {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, 55 | {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, 56 | {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, 57 | {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, 58 | {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, 59 | {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, 60 | {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, 61 | {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, 62 | {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, 63 | {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, 64 | {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, 65 | {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, 66 | {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, 67 | {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, 68 | ] 69 | 70 | [package.dependencies] 71 | click = ">=8.0.0" 72 | mypy-extensions = ">=0.4.3" 73 | packaging = ">=22.0" 74 | pathspec = ">=0.9.0" 75 | platformdirs = ">=2" 76 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 77 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 78 | 79 | [package.extras] 80 | colorama = ["colorama (>=0.4.3)"] 81 | d = ["aiohttp (>=3.10)"] 82 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 83 | uvloop = ["uvloop (>=0.15.2)"] 84 | 85 | [[package]] 86 | name = "cfgv" 87 | version = "3.4.0" 88 | description = "Validate configuration and produce human readable error messages." 89 | optional = false 90 | python-versions = ">=3.8" 91 | groups = ["dev"] 92 | files = [ 93 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 94 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 95 | ] 96 | 97 | [[package]] 98 | name = "click" 99 | version = "8.1.8" 100 | description = "Composable command line interface toolkit" 101 | optional = false 102 | python-versions = ">=3.7" 103 | groups = ["dev"] 104 | files = [ 105 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 106 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 107 | ] 108 | 109 | [package.dependencies] 110 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 111 | 112 | [[package]] 113 | name = "colorama" 114 | version = "0.4.6" 115 | description = "Cross-platform colored terminal text." 116 | optional = false 117 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 118 | groups = ["dev"] 119 | markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" 120 | files = [ 121 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 122 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 123 | ] 124 | 125 | [[package]] 126 | name = "coverage" 127 | version = "7.6.12" 128 | description = "Code coverage measurement for Python" 129 | optional = false 130 | python-versions = ">=3.9" 131 | groups = ["dev"] 132 | files = [ 133 | {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, 134 | {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, 135 | {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, 136 | {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, 137 | {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, 138 | {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, 139 | {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, 140 | {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, 141 | {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, 142 | {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, 143 | {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, 144 | {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, 145 | {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, 146 | {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, 147 | {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, 148 | {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, 149 | {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, 150 | {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, 151 | {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, 152 | {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, 153 | {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, 154 | {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, 155 | {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, 156 | {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, 157 | {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, 158 | {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, 159 | {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, 160 | {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, 161 | {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, 162 | {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, 163 | {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, 164 | {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, 165 | {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, 166 | {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, 167 | {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, 168 | {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, 169 | {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, 170 | {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, 171 | {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, 172 | {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, 173 | {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, 174 | {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, 175 | {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, 176 | {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, 177 | {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, 178 | {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, 179 | {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, 180 | {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, 181 | {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, 182 | {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, 183 | {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, 184 | {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, 185 | {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, 186 | {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, 187 | {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, 188 | {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, 189 | {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, 190 | {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, 191 | {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, 192 | {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, 193 | {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, 194 | {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, 195 | {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, 196 | ] 197 | 198 | [package.dependencies] 199 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 200 | 201 | [package.extras] 202 | toml = ["tomli"] 203 | 204 | [[package]] 205 | name = "distlib" 206 | version = "0.3.9" 207 | description = "Distribution utilities" 208 | optional = false 209 | python-versions = "*" 210 | groups = ["dev"] 211 | files = [ 212 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 213 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 214 | ] 215 | 216 | [[package]] 217 | name = "exceptiongroup" 218 | version = "1.2.2" 219 | description = "Backport of PEP 654 (exception groups)" 220 | optional = false 221 | python-versions = ">=3.7" 222 | groups = ["main", "dev"] 223 | markers = "python_version < \"3.11\"" 224 | files = [ 225 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 226 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 227 | ] 228 | 229 | [package.extras] 230 | test = ["pytest (>=6)"] 231 | 232 | [[package]] 233 | name = "execnet" 234 | version = "2.1.1" 235 | description = "execnet: rapid multi-Python deployment" 236 | optional = false 237 | python-versions = ">=3.8" 238 | groups = ["dev"] 239 | files = [ 240 | {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, 241 | {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, 242 | ] 243 | 244 | [package.extras] 245 | testing = ["hatch", "pre-commit", "pytest", "tox"] 246 | 247 | [[package]] 248 | name = "filelock" 249 | version = "3.17.0" 250 | description = "A platform independent file lock." 251 | optional = false 252 | python-versions = ">=3.9" 253 | groups = ["dev"] 254 | files = [ 255 | {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, 256 | {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, 257 | ] 258 | 259 | [package.extras] 260 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 261 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] 262 | typing = ["typing-extensions (>=4.12.2)"] 263 | 264 | [[package]] 265 | name = "identify" 266 | version = "2.6.8" 267 | description = "File identification library for Python" 268 | optional = false 269 | python-versions = ">=3.9" 270 | groups = ["dev"] 271 | files = [ 272 | {file = "identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255"}, 273 | {file = "identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc"}, 274 | ] 275 | 276 | [package.extras] 277 | license = ["ukkonen"] 278 | 279 | [[package]] 280 | name = "idna" 281 | version = "3.10" 282 | description = "Internationalized Domain Names in Applications (IDNA)" 283 | optional = false 284 | python-versions = ">=3.6" 285 | groups = ["main", "dev"] 286 | files = [ 287 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 288 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 289 | ] 290 | 291 | [package.extras] 292 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 293 | 294 | [[package]] 295 | name = "importlib-metadata" 296 | version = "8.6.1" 297 | description = "Read metadata from Python packages" 298 | optional = false 299 | python-versions = ">=3.9" 300 | groups = ["main"] 301 | files = [ 302 | {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, 303 | {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, 304 | ] 305 | 306 | [package.dependencies] 307 | zipp = ">=3.20" 308 | 309 | [package.extras] 310 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 311 | cover = ["pytest-cov"] 312 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 313 | enabler = ["pytest-enabler (>=2.2)"] 314 | perf = ["ipython"] 315 | test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] 316 | type = ["pytest-mypy"] 317 | 318 | [[package]] 319 | name = "iniconfig" 320 | version = "2.0.0" 321 | description = "brain-dead simple config-ini parsing" 322 | optional = false 323 | python-versions = ">=3.7" 324 | groups = ["dev"] 325 | files = [ 326 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 327 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 328 | ] 329 | 330 | [[package]] 331 | name = "izulu" 332 | version = "0.5.4" 333 | description = "The exceptional library" 334 | optional = false 335 | python-versions = ">=3.6" 336 | groups = ["main"] 337 | files = [ 338 | {file = "izulu-0.5.4-py3-none-any.whl", hash = "sha256:6431499a04f68daca0b852dfa5cfbcb7be804166bcdc4efd4dd4e6dd7a3e5898"}, 339 | {file = "izulu-0.5.4.tar.gz", hash = "sha256:a6619402ab3c04ca32bbfb5000138287691e0b47d9794ba55a10af403ed23644"}, 340 | ] 341 | 342 | [[package]] 343 | name = "mypy" 344 | version = "1.15.0" 345 | description = "Optional static typing for Python" 346 | optional = false 347 | python-versions = ">=3.9" 348 | groups = ["dev"] 349 | files = [ 350 | {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, 351 | {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, 352 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, 353 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, 354 | {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, 355 | {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, 356 | {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, 357 | {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, 358 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, 359 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, 360 | {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, 361 | {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, 362 | {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, 363 | {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, 364 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, 365 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, 366 | {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, 367 | {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, 368 | {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, 369 | {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, 370 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, 371 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, 372 | {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, 373 | {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, 374 | {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, 375 | {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, 376 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, 377 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, 378 | {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, 379 | {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, 380 | {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, 381 | {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, 382 | ] 383 | 384 | [package.dependencies] 385 | mypy_extensions = ">=1.0.0" 386 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 387 | typing_extensions = ">=4.6.0" 388 | 389 | [package.extras] 390 | dmypy = ["psutil (>=4.0)"] 391 | faster-cache = ["orjson"] 392 | install-types = ["pip"] 393 | mypyc = ["setuptools (>=50)"] 394 | reports = ["lxml"] 395 | 396 | [[package]] 397 | name = "mypy-extensions" 398 | version = "1.0.0" 399 | description = "Type system extensions for programs checked with the mypy type checker." 400 | optional = false 401 | python-versions = ">=3.5" 402 | groups = ["dev"] 403 | files = [ 404 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 405 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 406 | ] 407 | 408 | [[package]] 409 | name = "nodeenv" 410 | version = "1.9.1" 411 | description = "Node.js virtual environment builder" 412 | optional = false 413 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 414 | groups = ["dev"] 415 | files = [ 416 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 417 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 418 | ] 419 | 420 | [[package]] 421 | name = "packaging" 422 | version = "24.2" 423 | description = "Core utilities for Python packages" 424 | optional = false 425 | python-versions = ">=3.8" 426 | groups = ["main", "dev"] 427 | files = [ 428 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 429 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 430 | ] 431 | 432 | [[package]] 433 | name = "pathspec" 434 | version = "0.12.1" 435 | description = "Utility library for gitignore style pattern matching of file paths." 436 | optional = false 437 | python-versions = ">=3.8" 438 | groups = ["dev"] 439 | files = [ 440 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 441 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 442 | ] 443 | 444 | [[package]] 445 | name = "platformdirs" 446 | version = "4.3.6" 447 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 448 | optional = false 449 | python-versions = ">=3.8" 450 | groups = ["dev"] 451 | files = [ 452 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 453 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 454 | ] 455 | 456 | [package.extras] 457 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 458 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 459 | type = ["mypy (>=1.11.2)"] 460 | 461 | [[package]] 462 | name = "pluggy" 463 | version = "1.5.0" 464 | description = "plugin and hook calling mechanisms for python" 465 | optional = false 466 | python-versions = ">=3.8" 467 | groups = ["dev"] 468 | files = [ 469 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 470 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 471 | ] 472 | 473 | [package.extras] 474 | dev = ["pre-commit", "tox"] 475 | testing = ["pytest", "pytest-benchmark"] 476 | 477 | [[package]] 478 | name = "pre-commit" 479 | version = "4.1.0" 480 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 481 | optional = false 482 | python-versions = ">=3.9" 483 | groups = ["dev"] 484 | files = [ 485 | {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, 486 | {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, 487 | ] 488 | 489 | [package.dependencies] 490 | cfgv = ">=2.0.0" 491 | identify = ">=1.0.0" 492 | nodeenv = ">=0.11.1" 493 | pyyaml = ">=5.1" 494 | virtualenv = ">=20.10.0" 495 | 496 | [[package]] 497 | name = "psutil" 498 | version = "7.0.0" 499 | description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." 500 | optional = false 501 | python-versions = ">=3.6" 502 | groups = ["dev"] 503 | files = [ 504 | {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, 505 | {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, 506 | {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, 507 | {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, 508 | {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, 509 | {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, 510 | {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, 511 | {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, 512 | {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, 513 | {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, 514 | ] 515 | 516 | [package.extras] 517 | dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] 518 | test = ["pytest", "pytest-xdist", "setuptools"] 519 | 520 | [[package]] 521 | name = "pycron" 522 | version = "3.1.2" 523 | description = "Simple cron-like parser, which determines if current datetime matches conditions." 524 | optional = false 525 | python-versions = "<4.0,>=3.9" 526 | groups = ["main"] 527 | files = [ 528 | {file = "pycron-3.1.2-py3-none-any.whl", hash = "sha256:30e4a01889dfb471e80cc76a5c0ab87e675146a7f6d2d66a2e4bdb4e2975ef3d"}, 529 | {file = "pycron-3.1.2.tar.gz", hash = "sha256:114c71bc61002b850a416e33e72a8c6de75b9f5c4228e86d7babd8a7ea232d94"}, 530 | ] 531 | 532 | [[package]] 533 | name = "pydantic" 534 | version = "2.10.6" 535 | description = "Data validation using Python type hints" 536 | optional = false 537 | python-versions = ">=3.8" 538 | groups = ["main"] 539 | files = [ 540 | {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, 541 | {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, 542 | ] 543 | 544 | [package.dependencies] 545 | annotated-types = ">=0.6.0" 546 | pydantic-core = "2.27.2" 547 | typing-extensions = ">=4.12.2" 548 | 549 | [package.extras] 550 | email = ["email-validator (>=2.0.0)"] 551 | timezone = ["tzdata"] 552 | 553 | [[package]] 554 | name = "pydantic-core" 555 | version = "2.27.2" 556 | description = "Core functionality for Pydantic validation and serialization" 557 | optional = false 558 | python-versions = ">=3.8" 559 | groups = ["main"] 560 | files = [ 561 | {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, 562 | {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, 563 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, 564 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, 565 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, 566 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, 567 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, 568 | {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, 569 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, 570 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, 571 | {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, 572 | {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, 573 | {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, 574 | {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, 575 | {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, 576 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, 577 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, 578 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, 579 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, 580 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, 581 | {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, 582 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, 583 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, 584 | {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, 585 | {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, 586 | {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, 587 | {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, 588 | {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, 589 | {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, 590 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, 591 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, 592 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, 593 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, 594 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, 595 | {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, 596 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, 597 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, 598 | {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, 599 | {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, 600 | {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, 601 | {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, 602 | {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, 603 | {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, 604 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, 605 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, 606 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, 607 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, 608 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, 609 | {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, 610 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, 611 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, 612 | {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, 613 | {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, 614 | {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, 615 | {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, 616 | {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, 617 | {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, 618 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, 619 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, 620 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, 621 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, 622 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, 623 | {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, 624 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, 625 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, 626 | {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, 627 | {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, 628 | {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, 629 | {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, 630 | {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, 631 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, 632 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, 633 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, 634 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, 635 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, 636 | {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, 637 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, 638 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, 639 | {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, 640 | {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, 641 | {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, 642 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, 643 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, 644 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, 645 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, 646 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, 647 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, 648 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, 649 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, 650 | {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, 651 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, 652 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, 653 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, 654 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, 655 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, 656 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, 657 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, 658 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, 659 | {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, 660 | {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, 661 | ] 662 | 663 | [package.dependencies] 664 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 665 | 666 | [[package]] 667 | name = "pytest" 668 | version = "8.3.5" 669 | description = "pytest: simple powerful testing with Python" 670 | optional = false 671 | python-versions = ">=3.8" 672 | groups = ["dev"] 673 | files = [ 674 | {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, 675 | {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, 676 | ] 677 | 678 | [package.dependencies] 679 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 680 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 681 | iniconfig = "*" 682 | packaging = "*" 683 | pluggy = ">=1.5,<2" 684 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 685 | 686 | [package.extras] 687 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 688 | 689 | [[package]] 690 | name = "pytest-cov" 691 | version = "6.0.0" 692 | description = "Pytest plugin for measuring coverage." 693 | optional = false 694 | python-versions = ">=3.9" 695 | groups = ["dev"] 696 | files = [ 697 | {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, 698 | {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, 699 | ] 700 | 701 | [package.dependencies] 702 | coverage = {version = ">=7.5", extras = ["toml"]} 703 | pytest = ">=4.6" 704 | 705 | [package.extras] 706 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 707 | 708 | [[package]] 709 | name = "pytest-xdist" 710 | version = "3.6.1" 711 | description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" 712 | optional = false 713 | python-versions = ">=3.8" 714 | groups = ["dev"] 715 | files = [ 716 | {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, 717 | {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, 718 | ] 719 | 720 | [package.dependencies] 721 | execnet = ">=2.1" 722 | psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} 723 | pytest = ">=7.0.0" 724 | 725 | [package.extras] 726 | psutil = ["psutil (>=3.0)"] 727 | setproctitle = ["setproctitle"] 728 | testing = ["filelock"] 729 | 730 | [[package]] 731 | name = "pytz" 732 | version = "2025.1" 733 | description = "World timezone definitions, modern and historical" 734 | optional = false 735 | python-versions = "*" 736 | groups = ["main"] 737 | files = [ 738 | {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, 739 | {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, 740 | ] 741 | 742 | [[package]] 743 | name = "pyyaml" 744 | version = "6.0.2" 745 | description = "YAML parser and emitter for Python" 746 | optional = false 747 | python-versions = ">=3.8" 748 | groups = ["dev"] 749 | files = [ 750 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 751 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 752 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 753 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 754 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 755 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 756 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 757 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 758 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 759 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 760 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 761 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 762 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 763 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 764 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 765 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 766 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 767 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 768 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 769 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 770 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 771 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 772 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 773 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 774 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 775 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 776 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 777 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 778 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 779 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 780 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 781 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 782 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 783 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 784 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 785 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 786 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 787 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 788 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 789 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 790 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 791 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 792 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 793 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 794 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 795 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 796 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 797 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 798 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 799 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 800 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 801 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 802 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 803 | ] 804 | 805 | [[package]] 806 | name = "ruff" 807 | version = "0.9.9" 808 | description = "An extremely fast Python linter and code formatter, written in Rust." 809 | optional = false 810 | python-versions = ">=3.7" 811 | groups = ["dev"] 812 | files = [ 813 | {file = "ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367"}, 814 | {file = "ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7"}, 815 | {file = "ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d"}, 816 | {file = "ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a"}, 817 | {file = "ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe"}, 818 | {file = "ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c"}, 819 | {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be"}, 820 | {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590"}, 821 | {file = "ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb"}, 822 | {file = "ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0"}, 823 | {file = "ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17"}, 824 | {file = "ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1"}, 825 | {file = "ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57"}, 826 | {file = "ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e"}, 827 | {file = "ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1"}, 828 | {file = "ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1"}, 829 | {file = "ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf"}, 830 | {file = "ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933"}, 831 | ] 832 | 833 | [[package]] 834 | name = "sniffio" 835 | version = "1.3.1" 836 | description = "Sniff out which async library your code is running under" 837 | optional = false 838 | python-versions = ">=3.7" 839 | groups = ["main", "dev"] 840 | files = [ 841 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 842 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 843 | ] 844 | 845 | [[package]] 846 | name = "taskiq" 847 | version = "0.11.13" 848 | description = "Distributed task queue with full async support" 849 | optional = false 850 | python-versions = "<4.0,>=3.9" 851 | groups = ["main"] 852 | files = [ 853 | {file = "taskiq-0.11.13-py3-none-any.whl", hash = "sha256:df4db3c2d43b33360b1de18d9d7195397e553fa45b7a559be26236f08cecddad"}, 854 | {file = "taskiq-0.11.13.tar.gz", hash = "sha256:6d6d14807921b8ffa95a8418f992cf30edb9d8f9557c5f737c82ecaab8c387ea"}, 855 | ] 856 | 857 | [package.dependencies] 858 | anyio = ">=3" 859 | importlib-metadata = "*" 860 | izulu = "0.5.4" 861 | packaging = ">=19" 862 | pycron = ">=3.0.0,<4.0.0" 863 | pydantic = ">=1.0,<=3.0" 864 | pytz = "*" 865 | taskiq_dependencies = ">=1.3.1,<2" 866 | typing-extensions = ">=3.10.0.0" 867 | 868 | [package.extras] 869 | cbor = ["cbor2 (>=5,<6)"] 870 | metrics = ["prometheus_client (>=0,<1)"] 871 | msgpack = ["msgpack (>=1.0.7,<2.0.0)"] 872 | orjson = ["orjson (>=3,<4)"] 873 | reload = ["gitignore-parser (>=0,<1)", "watchdog (>=4,<5)"] 874 | uv = ["uvloop (>=0.16.0,<1)"] 875 | zmq = ["pyzmq (>=26,<27)"] 876 | 877 | [[package]] 878 | name = "taskiq-dependencies" 879 | version = "1.5.7" 880 | description = "FastAPI like dependency injection implementation" 881 | optional = false 882 | python-versions = "<4.0,>=3.9" 883 | groups = ["main"] 884 | files = [ 885 | {file = "taskiq_dependencies-1.5.7-py3-none-any.whl", hash = "sha256:6fcee5d159bdb035ef915d4d848826169b6f06fe57cc2297a39b62ea3e76036f"}, 886 | {file = "taskiq_dependencies-1.5.7.tar.gz", hash = "sha256:0d3b240872ef152b719153b9526d866d2be978aeeaea6600e878414babc2dcb4"}, 887 | ] 888 | 889 | [package.dependencies] 890 | typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.10\""} 891 | 892 | [[package]] 893 | name = "tomli" 894 | version = "2.2.1" 895 | description = "A lil' TOML parser" 896 | optional = false 897 | python-versions = ">=3.8" 898 | groups = ["dev"] 899 | markers = "python_full_version <= \"3.11.0a6\"" 900 | files = [ 901 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 902 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 903 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 904 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 905 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 906 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 907 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 908 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 909 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 910 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 911 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 912 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 913 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 914 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 915 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 916 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 917 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 918 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 919 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 920 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 921 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 922 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 923 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 924 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 925 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 926 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 927 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 928 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 929 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 930 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 931 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 932 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 933 | ] 934 | 935 | [[package]] 936 | name = "typing-extensions" 937 | version = "4.12.2" 938 | description = "Backported and Experimental Type Hints for Python 3.8+" 939 | optional = false 940 | python-versions = ">=3.8" 941 | groups = ["main", "dev"] 942 | files = [ 943 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 944 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 945 | ] 946 | 947 | [[package]] 948 | name = "virtualenv" 949 | version = "20.29.2" 950 | description = "Virtual Python Environment builder" 951 | optional = false 952 | python-versions = ">=3.8" 953 | groups = ["dev"] 954 | files = [ 955 | {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, 956 | {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, 957 | ] 958 | 959 | [package.dependencies] 960 | distlib = ">=0.3.7,<1" 961 | filelock = ">=3.12.2,<4" 962 | platformdirs = ">=3.9.1,<5" 963 | 964 | [package.extras] 965 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 966 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 967 | 968 | [[package]] 969 | name = "zipp" 970 | version = "3.21.0" 971 | description = "Backport of pathlib-compatible object wrapper for zip files" 972 | optional = false 973 | python-versions = ">=3.9" 974 | groups = ["main"] 975 | files = [ 976 | {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, 977 | {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, 978 | ] 979 | 980 | [package.extras] 981 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 982 | cover = ["pytest-cov"] 983 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 984 | enabler = ["pytest-enabler (>=2.2)"] 985 | test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] 986 | type = ["pytest-mypy"] 987 | 988 | [metadata] 989 | lock-version = "2.1" 990 | python-versions = "^3.9" 991 | content-hash = "774714a8589fc71bbf782225845287f408c185f1ca64b3c55355c00afb151c72" 992 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "taskiq-pipelines" 3 | # The version is set automatically by the CI/CD pipeline 4 | version = "0.0.0" 5 | description = "Taskiq pipelines for task chaining." 6 | authors = ["Pavel Kirilin "] 7 | readme = "README.md" 8 | repository = "https://github.com/taskiq-python/taskiq-pipelines" 9 | license = "LICENSE" 10 | classifiers = [ 11 | "Typing :: Typed", 12 | "Programming Language :: Python", 13 | "Programming Language :: Python :: 3", 14 | "Programming Language :: Python :: 3 :: Only", 15 | "Programming Language :: Python :: 3.9", 16 | "Programming Language :: Python :: 3.10", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | "Programming Language :: Python :: 3.13", 20 | "Operating System :: OS Independent", 21 | "Intended Audience :: Developers", 22 | "Topic :: System :: Networking", 23 | "Development Status :: 3 - Alpha", 24 | ] 25 | homepage = "https://github.com/taskiq-python/taskiq-pipelines" 26 | keywords = ["taskiq", "pipelines", "tasks", "distributed", "async"] 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.9" 30 | taskiq = ">=0.11.12, <1" 31 | typing-extensions = "^4.3.0" 32 | pydantic = "^2" 33 | 34 | [tool.poetry.group.dev.dependencies] 35 | pytest = "^8" 36 | black = { version = "^25", allow-prereleases = true } 37 | pytest-cov = "^6" 38 | anyio = "^4" 39 | pre-commit = "^4" 40 | mypy = "^1" 41 | pytest-xdist = { version = "^3", extras = ["psutil"] } 42 | ruff = "^0.9.9" 43 | 44 | [tool.mypy] 45 | strict = true 46 | ignore_missing_imports = true 47 | allow_subclassing_any = true 48 | allow_untyped_calls = true 49 | pretty = true 50 | show_error_codes = true 51 | implicit_reexport = true 52 | allow_untyped_decorators = true 53 | warn_return_any = false 54 | warn_unused_ignores = false 55 | 56 | [tool.isort] 57 | profile = "black" 58 | multi_line_output = 3 59 | 60 | [build-system] 61 | requires = ["poetry-core>=1.0.0"] 62 | build-backend = "poetry.core.masonry.api" 63 | 64 | [tool.ruff] 65 | # List of enabled rulsets. 66 | # See https://docs.astral.sh/ruff/rules/ for more information. 67 | lint.select = [ 68 | "E", # Error 69 | "F", # Pyflakes 70 | "W", # Pycodestyle 71 | "C90", # McCabe complexity 72 | "I", # Isort 73 | "N", # pep8-naming 74 | "D", # Pydocstyle 75 | "ANN", # Pytype annotations 76 | "S", # Bandit 77 | "B", # Bugbear 78 | "COM", # Commas 79 | "C4", # Comprehensions 80 | "ISC", # Implicit string concat 81 | "PIE", # Unnecessary code 82 | "T20", # Catch prints 83 | "PYI", # validate pyi files 84 | "Q", # Checks for quotes 85 | "RSE", # Checks raise statements 86 | "RET", # Checks return statements 87 | "SLF", # Self checks 88 | "SIM", # Simplificator 89 | "PTH", # Pathlib checks 90 | "ERA", # Checks for commented out code 91 | "PL", # PyLint checks 92 | "RUF", # Specific to Ruff checks 93 | ] 94 | lint.ignore = [ 95 | "D105", # Missing docstring in magic method 96 | "D107", # Missing docstring in __init__ 97 | "D212", # Multi-line docstring summary should start at the first line 98 | "D401", # First line should be in imperative mood 99 | "D104", # Missing docstring in public package 100 | "D100", # Missing docstring in public module 101 | "ANN401", # typing.Any are disallowed in `**kwargs 102 | "PLR0913", # Too many arguments for function call 103 | "D106", # Missing docstring in public nested class 104 | "SLF001", # Private member accessed 105 | ] 106 | exclude = [".venv/"] 107 | lint.mccabe = { max-complexity = 10 } 108 | line-length = 88 109 | 110 | [tool.ruff.lint.per-file-ignores] 111 | "tests/*" = [ 112 | "S101", # Use of assert detected 113 | "S301", # Use of pickle detected 114 | "D103", # Missing docstring in public function 115 | "SLF001", # Private member accessed 116 | "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes 117 | "D101", # Missing docstring in public class 118 | ] 119 | 120 | [tool.ruff.lint.pydocstyle] 121 | convention = "pep257" 122 | ignore-decorators = ["typing.overload"] 123 | 124 | [tool.ruff.lint.pylint] 125 | allow-magic-value-types = ["int", "str", "float"] 126 | 127 | [tool.ruff.lint.flake8-bugbear] 128 | extend-immutable-calls = ["taskiq_dependencies.Depends", "taskiq.TaskiqDepends"] 129 | -------------------------------------------------------------------------------- /taskiq_pipelines/__init__.py: -------------------------------------------------------------------------------- 1 | """Pipelines for taskiq tasks.""" 2 | 3 | from taskiq_pipelines.exceptions import AbortPipeline, PipelineError 4 | from taskiq_pipelines.middleware import PipelineMiddleware 5 | from taskiq_pipelines.pipeliner import Pipeline 6 | 7 | __all__ = [ 8 | "AbortPipeline", 9 | "Pipeline", 10 | "PipelineError", 11 | "PipelineMiddleware", 12 | ] 13 | -------------------------------------------------------------------------------- /taskiq_pipelines/abc.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, Type 3 | 4 | from taskiq import AsyncBroker, TaskiqResult 5 | from typing_extensions import ClassVar 6 | 7 | 8 | class AbstractStep(ABC): 9 | """Abstract pipeline step.""" 10 | 11 | _step_name: str 12 | _known_steps: ClassVar[Dict[str, Type["AbstractStep"]]] = {} 13 | 14 | def __init_subclass__(cls, step_name: str, **kwargs: Any) -> None: 15 | super().__init_subclass__(**kwargs) 16 | # Sets step name to the step. 17 | cls._step_name = step_name 18 | # Registers new subclass in the dict of 19 | # known steps. 20 | cls._known_steps[step_name] = cls 21 | 22 | @abstractmethod 23 | async def act( 24 | self, 25 | broker: AsyncBroker, 26 | step_number: int, 27 | parent_task_id: str, 28 | task_id: str, 29 | pipe_data: str, 30 | result: "TaskiqResult[Any]", 31 | ) -> None: 32 | """ 33 | Perform pipeline action. 34 | 35 | If you create task, please 36 | assign given task_id to this task, 37 | it helps clients to identify currently 38 | executed task. 39 | 40 | :param broker: current broker. 41 | :param step_number: current step number. 42 | :param parent_task_id: current task id. 43 | :param task_id: task_id to use. 44 | :param pipe_data: serialized pipeline must be in labels. 45 | :param result: result of a previous task. 46 | """ 47 | -------------------------------------------------------------------------------- /taskiq_pipelines/constants.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | CURRENT_STEP = "_pipe_current_step" 4 | PIPELINE_DATA = "_pipe_data" 5 | 6 | EMPTY_PARAM_NAME: Literal[-1] = -1 7 | -------------------------------------------------------------------------------- /taskiq_pipelines/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar, Union 2 | 3 | from taskiq import TaskiqError 4 | 5 | 6 | class PipelineError(TaskiqError): 7 | """Generic pipeline error.""" 8 | 9 | 10 | class StepError(PipelineError): 11 | """Error found while mapping step.""" 12 | 13 | __template__ = ( 14 | "Task {task_id} returned an error. {_STEP_NAME} failed. Reason: {error}" 15 | ) 16 | _STEP_NAME: ClassVar[str] 17 | 18 | task_id: str 19 | error: Union[BaseException, None] 20 | 21 | 22 | class MappingError(StepError): 23 | """Error found while mapping step.""" 24 | 25 | _STEP_NAME = "mapping" 26 | 27 | 28 | class FilterError(StepError): 29 | """Error found while filtering step.""" 30 | 31 | _STEP_NAME = "filtering" 32 | 33 | 34 | class AbortPipeline(PipelineError): # noqa: N818 35 | """ 36 | Abort curret pipeline execution. 37 | 38 | This error can be thrown from 39 | act method of a step. 40 | 41 | It imediately aborts current pipeline 42 | execution. 43 | """ 44 | 45 | __template__ = "Pipeline was aborted. {reason}" 46 | 47 | reason: str = "No reason provided." 48 | -------------------------------------------------------------------------------- /taskiq_pipelines/middleware.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from typing import Any, List, Optional 3 | 4 | import pydantic 5 | from taskiq import TaskiqMessage, TaskiqMiddleware, TaskiqResult 6 | 7 | from taskiq_pipelines.constants import CURRENT_STEP, PIPELINE_DATA 8 | from taskiq_pipelines.exceptions import AbortPipeline 9 | from taskiq_pipelines.pipeliner import DumpedStep 10 | from taskiq_pipelines.steps import parse_step 11 | 12 | logger = getLogger(__name__) 13 | 14 | 15 | class PipelineMiddleware(TaskiqMiddleware): 16 | """Pipeline middleware.""" 17 | 18 | async def post_save( # noqa: PLR0911 19 | self, 20 | message: "TaskiqMessage", 21 | result: "TaskiqResult[Any]", 22 | ) -> None: 23 | """ 24 | Handle post-execute event. 25 | 26 | This is the heart of pipelines. 27 | Here we decide what to do next. 28 | 29 | If the message have pipeline 30 | labels we can calculate our next step. 31 | 32 | :param message: current message. 33 | :param result: result of the execution. 34 | """ 35 | if result.is_err: 36 | return 37 | if CURRENT_STEP not in message.labels: 38 | return 39 | current_step_num = int(message.labels[CURRENT_STEP]) 40 | if PIPELINE_DATA not in message.labels: 41 | logger.warning("Pipline data not found. Execution flow is broken.") 42 | return 43 | pipeline_data = message.labels[PIPELINE_DATA] 44 | parsed_data = self.broker.serializer.loadb(pipeline_data) 45 | try: 46 | steps_data = pydantic.TypeAdapter(List[DumpedStep]).validate_python( 47 | parsed_data, 48 | ) 49 | except ValueError as err: 50 | logger.warning("Cannot parse pipline_data: %s", err, exc_info=True) 51 | return 52 | if current_step_num + 1 >= len(steps_data): 53 | logger.debug("Pipeline is completed.") 54 | return 55 | next_step_data = steps_data[current_step_num + 1] 56 | try: 57 | next_step = parse_step( 58 | step_type=next_step_data.step_type, 59 | step_data=next_step_data.step_data, 60 | ) 61 | except ValueError as exc: 62 | logger.warning("Cannot parse step data.") 63 | logger.debug("%s", exc, exc_info=True) 64 | return 65 | 66 | try: 67 | await next_step.act( 68 | broker=self.broker, 69 | step_number=current_step_num + 1, 70 | parent_task_id=message.task_id, 71 | task_id=next_step_data.task_id, 72 | pipe_data=pipeline_data, 73 | result=result, 74 | ) 75 | except AbortPipeline as abort_exc: 76 | logger.warning( 77 | "Pipeline is aborted. Reason: %s", 78 | abort_exc, 79 | exc_info=True, 80 | ) 81 | if current_step_num == len(steps_data) - 1: 82 | return 83 | await self.fail_pipeline(steps_data[-1].task_id) 84 | 85 | async def on_error( 86 | self, 87 | message: "TaskiqMessage", 88 | result: "TaskiqResult[Any]", 89 | exception: BaseException, 90 | ) -> None: 91 | """ 92 | Handles on_error event. 93 | 94 | :param message: current message. 95 | :param result: execution result. 96 | :param exception: found exception. 97 | """ 98 | if CURRENT_STEP not in message.labels: 99 | return 100 | current_step_num = int(message.labels[CURRENT_STEP]) 101 | if PIPELINE_DATA not in message.labels: 102 | logger.warning("Pipline data not found. Execution flow is broken.") 103 | return 104 | pipe_data = message.labels[PIPELINE_DATA] 105 | try: 106 | steps = pydantic.TypeAdapter(List[DumpedStep]).validate_json(pipe_data) 107 | except ValueError: 108 | return 109 | if current_step_num == len(steps) - 1: 110 | return 111 | await self.fail_pipeline(steps[-1].task_id, result.error) 112 | 113 | async def fail_pipeline( 114 | self, 115 | last_task_id: str, 116 | abort: Optional[BaseException] = None, 117 | ) -> None: 118 | """ 119 | This function aborts pipeline. 120 | 121 | This is done by setting error result for 122 | the last task in the pipeline. 123 | 124 | :param last_task_id: id of the last task. 125 | :param abort: caught earlier exception or default 126 | """ 127 | await self.broker.result_backend.set_result( 128 | last_task_id, 129 | TaskiqResult( 130 | is_err=True, 131 | return_value=None, # type: ignore 132 | error=abort or AbortPipeline(reason="Execution aborted."), 133 | execution_time=0, 134 | log="Error found while executing pipeline.", 135 | ), 136 | ) 137 | -------------------------------------------------------------------------------- /taskiq_pipelines/pipeliner.py: -------------------------------------------------------------------------------- 1 | from types import CoroutineType 2 | from typing import ( 3 | Any, 4 | Coroutine, 5 | Dict, 6 | Generic, 7 | List, 8 | Literal, 9 | Optional, 10 | TypeVar, 11 | Union, 12 | overload, 13 | ) 14 | 15 | import pydantic 16 | from taskiq import AsyncBroker, AsyncTaskiqTask 17 | from taskiq.decor import AsyncTaskiqDecoratedTask 18 | from taskiq.kicker import AsyncKicker 19 | from typing_extensions import ParamSpec 20 | 21 | from taskiq_pipelines.constants import CURRENT_STEP, EMPTY_PARAM_NAME, PIPELINE_DATA 22 | from taskiq_pipelines.steps import FilterStep, MapperStep, SequentialStep, parse_step 23 | 24 | _ReturnType = TypeVar("_ReturnType") 25 | _FuncParams = ParamSpec("_FuncParams") 26 | _T2 = TypeVar("_T2") 27 | 28 | 29 | class DumpedStep(pydantic.BaseModel): 30 | """Dumped state model.""" 31 | 32 | step_type: str 33 | step_data: Dict[str, Any] 34 | task_id: str 35 | 36 | 37 | DumpedSteps = pydantic.RootModel[List[DumpedStep]] 38 | 39 | 40 | class Pipeline(Generic[_FuncParams, _ReturnType]): 41 | """ 42 | Pipeline constructor. 43 | 44 | This class helps you to build pipelines. 45 | It creates all needed data and manages 46 | task ids. Also it has helper methods, 47 | to easily add new pipeline steps. 48 | 49 | Of course it can be done manually, 50 | but it's nice to have. 51 | """ 52 | 53 | def __init__( 54 | self, 55 | broker: AsyncBroker, 56 | task: Optional[ 57 | Union[ 58 | AsyncKicker[_FuncParams, _ReturnType], 59 | AsyncTaskiqDecoratedTask[_FuncParams, _ReturnType], 60 | ] 61 | ] = None, 62 | ) -> None: 63 | self.broker = broker 64 | self.steps: "List[DumpedStep]" = [] 65 | if task: 66 | self.call_next(task) 67 | 68 | @overload 69 | def call_next( 70 | self: "Pipeline[_FuncParams, _ReturnType]", 71 | task: Union[ 72 | AsyncKicker[Any, Coroutine[Any, Any, _T2]], 73 | AsyncKicker[Any, "CoroutineType[Any, Any, _T2]"], 74 | AsyncTaskiqDecoratedTask[Any, Coroutine[Any, Any, _T2]], 75 | AsyncTaskiqDecoratedTask[Any, "CoroutineType[Any, Any, _T2]"], 76 | ], 77 | param_name: Union[Optional[str], Literal[-1]] = None, 78 | **additional_kwargs: Any, 79 | ) -> "Pipeline[_FuncParams, _T2]": ... 80 | 81 | @overload 82 | def call_next( 83 | self: "Pipeline[_FuncParams, _ReturnType]", 84 | task: Union[ 85 | AsyncKicker[Any, _T2], 86 | AsyncTaskiqDecoratedTask[Any, _T2], 87 | ], 88 | param_name: Union[Optional[str], Literal[-1]] = None, 89 | **additional_kwargs: Any, 90 | ) -> "Pipeline[_FuncParams, _T2]": ... 91 | 92 | def call_next( 93 | self, 94 | task: Union[ 95 | AsyncKicker[Any, Any], 96 | AsyncTaskiqDecoratedTask[Any, Any], 97 | ], 98 | param_name: Union[Optional[str], Literal[-1]] = None, 99 | **additional_kwargs: Any, 100 | ) -> Any: 101 | """ 102 | Adds sequential step. 103 | 104 | This task will be executed right after 105 | the previous and result of the previous task 106 | will be passed as the first argument, 107 | or it will be passed as key word argument, 108 | if param_name is specified. 109 | 110 | :param task: task to execute. 111 | :param param_name: kwarg param name, defaults to None. 112 | If set to -1 (EMPTY_PARAM_NAME), result is not passed. 113 | :param additional_kwargs: additional kwargs to task. 114 | :return: updated pipeline. 115 | """ 116 | self.steps.append( 117 | DumpedStep( 118 | step_type=SequentialStep._step_name, 119 | step_data=SequentialStep.from_task( 120 | task=task, 121 | param_name=param_name, 122 | **additional_kwargs, 123 | ).model_dump(), 124 | task_id="", 125 | ), 126 | ) 127 | return self 128 | 129 | @overload 130 | def call_after( 131 | self: "Pipeline[_FuncParams, _ReturnType]", 132 | task: Union[ 133 | AsyncKicker[Any, Coroutine[Any, Any, _T2]], 134 | AsyncKicker[Any, "CoroutineType[Any, Any, _T2]"], 135 | AsyncTaskiqDecoratedTask[Any, Coroutine[Any, Any, _T2]], 136 | AsyncTaskiqDecoratedTask[Any, "CoroutineType[Any, Any, _T2]"], 137 | ], 138 | **additional_kwargs: Any, 139 | ) -> "Pipeline[_FuncParams, _T2]": ... 140 | 141 | @overload 142 | def call_after( 143 | self: "Pipeline[_FuncParams, _ReturnType]", 144 | task: Union[ 145 | AsyncKicker[Any, _T2], 146 | AsyncTaskiqDecoratedTask[Any, _T2], 147 | ], 148 | **additional_kwargs: Any, 149 | ) -> "Pipeline[_FuncParams, _T2]": ... 150 | 151 | def call_after( 152 | self, 153 | task: Union[ 154 | AsyncKicker[Any, Any], 155 | AsyncTaskiqDecoratedTask[Any, Any], 156 | ], 157 | **additional_kwargs: Any, 158 | ) -> Any: 159 | """ 160 | Adds sequential step. 161 | 162 | This task will be executed right after 163 | the previous and result of the previous task 164 | is not passed to the next task. 165 | 166 | This is equivalent to call_next(task, param_name=-1). 167 | 168 | :param task: task to execute. 169 | :param additional_kwargs: additional kwargs to task. 170 | :return: updated pipeline. 171 | """ 172 | self.steps.append( 173 | DumpedStep( 174 | step_type=SequentialStep._step_name, 175 | step_data=SequentialStep.from_task( 176 | task=task, 177 | param_name=EMPTY_PARAM_NAME, 178 | **additional_kwargs, 179 | ).model_dump(), 180 | task_id="", 181 | ), 182 | ) 183 | return self 184 | 185 | @overload 186 | def map( 187 | self: "Pipeline[_FuncParams, _ReturnType]", 188 | task: Union[ 189 | AsyncKicker[Any, Coroutine[Any, Any, _T2]], 190 | AsyncKicker[Any, "CoroutineType[Any, Any, _T2]"], 191 | AsyncTaskiqDecoratedTask[Any, Coroutine[Any, Any, _T2]], 192 | AsyncTaskiqDecoratedTask[Any, "CoroutineType[Any, Any, _T2]"], 193 | ], 194 | param_name: Optional[str] = None, 195 | skip_errors: bool = False, 196 | check_interval: float = 0.5, 197 | **additional_kwargs: Any, 198 | ) -> "Pipeline[_FuncParams, List[_T2]]": ... 199 | 200 | @overload 201 | def map( 202 | self: "Pipeline[_FuncParams, _ReturnType]", 203 | task: Union[ 204 | AsyncKicker[Any, _T2], 205 | AsyncTaskiqDecoratedTask[Any, _T2], 206 | ], 207 | param_name: Optional[str] = None, 208 | skip_errors: bool = False, 209 | check_interval: float = 0.5, 210 | **additional_kwargs: Any, 211 | ) -> "Pipeline[_FuncParams, List[_T2]]": ... 212 | 213 | def map( 214 | self, 215 | task: Union[ 216 | AsyncKicker[Any, Any], 217 | AsyncTaskiqDecoratedTask[Any, Any], 218 | ], 219 | param_name: Optional[str] = None, 220 | skip_errors: bool = False, 221 | check_interval: float = 0.5, 222 | **additional_kwargs: Any, 223 | ) -> Any: 224 | """ 225 | Create new map task. 226 | 227 | This task is used to map values of an 228 | iterable. 229 | 230 | It creates many subtasks and then collects 231 | all results. 232 | 233 | :param task: task to execute on each value of an iterable. 234 | :param param_name: param name to use to inject the result of 235 | the previous task. If none, result injected as the first argument. 236 | :param skip_errors: skip error results, defaults to False. 237 | :param check_interval: how often task completion is checked. 238 | :param additional_kwargs: additional function's kwargs. 239 | :return: pipeline. 240 | """ 241 | self.steps.append( 242 | DumpedStep( 243 | step_type=MapperStep._step_name, 244 | step_data=MapperStep.from_task( 245 | task=task, 246 | param_name=param_name, 247 | skip_errors=skip_errors, 248 | check_interval=check_interval, 249 | **additional_kwargs, 250 | ).model_dump(), 251 | task_id="", 252 | ), 253 | ) 254 | return self 255 | 256 | @overload 257 | def filter( 258 | self: "Pipeline[_FuncParams, _ReturnType]", 259 | task: Union[ 260 | AsyncKicker[Any, Coroutine[Any, Any, bool]], 261 | AsyncKicker[Any, "CoroutineType[Any, Any, bool]"], 262 | AsyncTaskiqDecoratedTask[Any, Coroutine[Any, Any, bool]], 263 | AsyncTaskiqDecoratedTask[Any, "CoroutineType[Any, Any, bool]"], 264 | ], 265 | param_name: Optional[str] = None, 266 | skip_errors: bool = False, 267 | check_interval: float = 0.5, 268 | **additional_kwargs: Any, 269 | ) -> "Pipeline[_FuncParams, _ReturnType]": ... 270 | 271 | @overload 272 | def filter( 273 | self: "Pipeline[_FuncParams, _ReturnType]", 274 | task: Union[ 275 | AsyncKicker[Any, bool], 276 | AsyncTaskiqDecoratedTask[Any, bool], 277 | ], 278 | param_name: Optional[str] = None, 279 | skip_errors: bool = False, 280 | check_interval: float = 0.5, 281 | **additional_kwargs: Any, 282 | ) -> "Pipeline[_FuncParams, _ReturnType]": ... 283 | 284 | def filter( 285 | self, 286 | task: Union[ 287 | AsyncKicker[Any, Any], 288 | AsyncTaskiqDecoratedTask[Any, Any], 289 | ], 290 | param_name: Optional[str] = None, 291 | skip_errors: bool = False, 292 | check_interval: float = 0.5, 293 | **additional_kwargs: Any, 294 | ) -> Any: 295 | """ 296 | Add filter step. 297 | 298 | This step is executed on a list of items, 299 | like map. 300 | 301 | It runs many small subtasks for each item 302 | in sequence and if task returns true, 303 | the result is added to the final list. 304 | 305 | :param task: task to execute on every item. 306 | :param param_name: parameter name to pass item into, defaults to None 307 | :param skip_errors: skip errors if any, defaults to False 308 | :param check_interval: how often the result of all subtasks is checked, 309 | defaults to 0.5 310 | :param additional_kwargs: additional function's kwargs. 311 | :return: pipeline with filtering step. 312 | """ 313 | self.steps.append( 314 | DumpedStep( 315 | step_type=FilterStep._step_name, 316 | step_data=FilterStep.from_task( 317 | task=task, 318 | param_name=param_name, 319 | skip_errors=skip_errors, 320 | check_interval=check_interval, 321 | **additional_kwargs, 322 | ).model_dump(), 323 | task_id="", 324 | ), 325 | ) 326 | return self 327 | 328 | def dumpb(self) -> bytes: 329 | """ 330 | Dumps current pipeline as string. 331 | 332 | :returns: serialized pipeline. 333 | """ 334 | return self.broker.serializer.dumpb( 335 | DumpedSteps.model_validate(self.steps).model_dump(), 336 | ) 337 | 338 | @classmethod 339 | def loadb(cls, broker: AsyncBroker, pipe_data: bytes) -> "Pipeline[Any, Any]": 340 | """ 341 | Parses serialized pipeline. 342 | 343 | This method requires broker, 344 | to make pipeline kickable. 345 | 346 | :param broker: broker to use when call kiq. 347 | :param pipe_data: serialized pipeline data. 348 | :return: new 349 | """ 350 | pipe: "Pipeline[Any, Any]" = Pipeline(broker) 351 | data = broker.serializer.loadb(pipe_data) 352 | pipe.steps = DumpedSteps.model_validate(data) # type: ignore[assignment] 353 | return pipe 354 | 355 | async def kiq( 356 | self, 357 | *args: _FuncParams.args, 358 | **kwargs: _FuncParams.kwargs, 359 | ) -> AsyncTaskiqTask[_ReturnType]: 360 | """ 361 | Kiq pipeline. 362 | 363 | This function is used as kiq in functions, 364 | but it saves current pipeline as 365 | custom label, so worker can understand, 366 | what to do next. 367 | 368 | :param args: first function's args. 369 | :param kwargs: first function's kwargs. 370 | 371 | :raises ValueError: if pipe is empty, or 372 | first step isn't sequential. 373 | 374 | :return: TaskqTask for the final function. 375 | """ 376 | if not self.steps: 377 | raise ValueError("Pipeline is empty.") 378 | self._update_task_ids() 379 | step = self.steps[0] 380 | parsed_step = parse_step(step.step_type, step.step_data) 381 | if not isinstance(parsed_step, SequentialStep): 382 | raise ValueError("First step must be sequential.") 383 | kicker = ( 384 | AsyncKicker( 385 | parsed_step.task_name, 386 | broker=self.broker, 387 | labels=parsed_step.labels, 388 | ) 389 | .with_task_id(step.task_id) 390 | .with_labels( 391 | **{CURRENT_STEP: 0, PIPELINE_DATA: self.dumpb()}, # type: ignore 392 | ) 393 | ) 394 | taskiq_task = await kicker.kiq(*args, **kwargs) 395 | taskiq_task.task_id = self.steps[-1].task_id 396 | return taskiq_task 397 | 398 | def _update_task_ids(self) -> None: 399 | """Calculates task ids for each step in the pipeline.""" 400 | for step in self.steps: 401 | step.task_id = self.broker.id_generator() 402 | -------------------------------------------------------------------------------- /taskiq_pipelines/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taskiq-python/taskiq-pipelines/ef097a5baa3caf66b065fdddf5fbcbcac0b75532/taskiq_pipelines/py.typed -------------------------------------------------------------------------------- /taskiq_pipelines/steps/__init__.py: -------------------------------------------------------------------------------- 1 | """Package with default pipeline steps.""" 2 | 3 | from logging import getLogger 4 | from typing import Any, Dict 5 | 6 | from taskiq_pipelines.abc import AbstractStep 7 | from taskiq_pipelines.steps.filter import FilterStep 8 | from taskiq_pipelines.steps.mapper import MapperStep 9 | from taskiq_pipelines.steps.sequential import SequentialStep 10 | 11 | logger = getLogger(__name__) 12 | 13 | 14 | def parse_step(step_type: str, step_data: Dict[str, Any]) -> AbstractStep: 15 | step_cls = AbstractStep._known_steps.get(step_type) 16 | if step_cls is None: 17 | logger.warning(f"Unknown step type: {step_type}") 18 | raise ValueError("Unknown step type.") 19 | return step_cls(**step_data) 20 | 21 | 22 | __all__ = [ 23 | "FilterStep", 24 | "MapperStep", 25 | "SequentialStep", 26 | ] 27 | -------------------------------------------------------------------------------- /taskiq_pipelines/steps/filter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Dict, Iterable, List, Optional, Union 3 | 4 | import pydantic 5 | from taskiq import AsyncBroker, Context, TaskiqDepends, TaskiqResult 6 | from taskiq.brokers.shared_broker import async_shared_broker 7 | from taskiq.decor import AsyncTaskiqDecoratedTask 8 | from taskiq.kicker import AsyncKicker 9 | 10 | from taskiq_pipelines.abc import AbstractStep 11 | from taskiq_pipelines.constants import CURRENT_STEP, PIPELINE_DATA 12 | from taskiq_pipelines.exceptions import AbortPipeline, FilterError 13 | 14 | 15 | @async_shared_broker.task(task_name="taskiq_pipelines.shared.filter_tasks") 16 | async def filter_tasks( # noqa: C901 17 | task_ids: List[str], 18 | parent_task_id: str, 19 | check_interval: float, 20 | skip_errors: bool = False, 21 | context: Context = TaskiqDepends(), 22 | ) -> List[Any]: 23 | """ 24 | Filter resulted tasks. 25 | 26 | It takes list of task ids, 27 | and parent task id. 28 | 29 | After all subtasks are completed it gets 30 | result of a parent task, and 31 | if subtask's result of execution can be 32 | converted to True, the item from the original 33 | tasks is added to the resulting array. 34 | 35 | :param task_ids: ordered list of task ids. 36 | :param parent_task_id: task id of a parent task. 37 | :param check_interval: how often checks are performed. 38 | :param context: context of the execution, defaults to default_context 39 | :param skip_errors: skip errors of subtasks, defaults to False 40 | :raises TaskiqError: if any subtask has returned error. 41 | :return: fitlered results. 42 | """ 43 | ordered_ids = task_ids[:] 44 | tasks_set = set(task_ids) 45 | while tasks_set: 46 | for task_id in task_ids: 47 | if await context.broker.result_backend.is_result_ready(task_id): 48 | try: 49 | tasks_set.remove(task_id) 50 | except LookupError: 51 | continue 52 | if tasks_set: 53 | await asyncio.sleep(check_interval) 54 | 55 | results = await context.broker.result_backend.get_result(parent_task_id) 56 | filtered_results = [] 57 | for task_id, value in zip( 58 | ordered_ids, 59 | results.return_value, # type: ignore 60 | ): 61 | result = await context.broker.result_backend.get_result(task_id) 62 | if result.is_err: 63 | if skip_errors: 64 | continue 65 | err_cause = None 66 | if isinstance(result.error, BaseException): 67 | err_cause = result.error 68 | raise FilterError(task_id=task_id, error=result.error) from err_cause 69 | 70 | if result.return_value: 71 | filtered_results.append(value) 72 | return filtered_results 73 | 74 | 75 | class FilterStep(pydantic.BaseModel, AbstractStep, step_name="filter"): 76 | """Task to filter results.""" 77 | 78 | task_name: str 79 | labels: Dict[str, str] 80 | param_name: Optional[str] 81 | additional_kwargs: Dict[str, Any] 82 | skip_errors: bool 83 | check_interval: float 84 | 85 | async def act( 86 | self, 87 | broker: AsyncBroker, 88 | step_number: int, 89 | parent_task_id: str, 90 | task_id: str, 91 | pipe_data: str, 92 | result: "TaskiqResult[Any]", 93 | ) -> None: 94 | """ 95 | Run filter action. 96 | 97 | This function creates many small filter steps, 98 | and then collects all results in one big filtered array, 99 | using 'filter_tasks' shared task. 100 | 101 | :param broker: current broker. 102 | :param step_number: current step number. 103 | :param parent_task_id: task_id of the previous step. 104 | :param task_id: task_id to use in this step. 105 | :param pipe_data: serialized pipeline. 106 | :param result: result of the previous task. 107 | :raises AbortPipeline: if result is not iterable. 108 | """ 109 | if not isinstance(result.return_value, Iterable): 110 | raise AbortPipeline(reason="Result of the previous task is not iterable.") 111 | sub_task_ids = [] 112 | for item in result.return_value: 113 | kicker: "AsyncKicker[Any, Any]" = AsyncKicker( 114 | task_name=self.task_name, 115 | broker=broker, 116 | labels=self.labels, 117 | ) 118 | if self.param_name: 119 | self.additional_kwargs[self.param_name] = item 120 | task = await kicker.kiq(**self.additional_kwargs) 121 | else: 122 | task = await kicker.kiq(item, **self.additional_kwargs) 123 | sub_task_ids.append(task.task_id) 124 | await ( 125 | filter_tasks.kicker() 126 | .with_task_id(task_id) 127 | .with_broker( 128 | broker, 129 | ) 130 | .with_labels( 131 | **{CURRENT_STEP: step_number, PIPELINE_DATA: pipe_data}, # type: ignore 132 | ) 133 | .kiq( 134 | sub_task_ids, 135 | parent_task_id, 136 | check_interval=self.check_interval, 137 | skip_errors=self.skip_errors, 138 | ) 139 | ) 140 | 141 | @classmethod 142 | def from_task( 143 | cls, 144 | task: Union[ 145 | AsyncKicker[Any, Any], 146 | AsyncTaskiqDecoratedTask[Any, Any], 147 | ], 148 | param_name: Optional[str], 149 | skip_errors: bool, 150 | check_interval: float, 151 | **additional_kwargs: Any, 152 | ) -> "FilterStep": 153 | """ 154 | Create new filter step from task. 155 | 156 | :param task: task to execute. 157 | :param param_name: parameter name. 158 | :param skip_errors: don't fail collector 159 | task on errors. 160 | :param check_interval: how often tasks are checked. 161 | :param additional_kwargs: additional function's kwargs. 162 | :return: new mapper step. 163 | """ 164 | kicker = task.kicker() if isinstance(task, AsyncTaskiqDecoratedTask) else task 165 | message = kicker._prepare_message() 166 | return FilterStep( 167 | task_name=message.task_name, 168 | labels=message.labels, 169 | param_name=param_name, 170 | additional_kwargs=additional_kwargs, 171 | skip_errors=skip_errors, 172 | check_interval=check_interval, 173 | ) 174 | -------------------------------------------------------------------------------- /taskiq_pipelines/steps/mapper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Dict, Iterable, List, Optional, Union 3 | 4 | import pydantic 5 | from taskiq import ( 6 | AsyncBroker, 7 | AsyncTaskiqDecoratedTask, 8 | Context, 9 | TaskiqDepends, 10 | TaskiqResult, 11 | async_shared_broker, 12 | ) 13 | from taskiq.kicker import AsyncKicker 14 | 15 | from taskiq_pipelines.abc import AbstractStep 16 | from taskiq_pipelines.constants import CURRENT_STEP, PIPELINE_DATA 17 | from taskiq_pipelines.exceptions import AbortPipeline, MappingError 18 | 19 | 20 | @async_shared_broker.task(task_name="taskiq_pipelines.shared.wait_tasks") 21 | async def wait_tasks( 22 | task_ids: List[str], 23 | check_interval: float, 24 | skip_errors: bool = True, 25 | context: Context = TaskiqDepends(), 26 | ) -> List[Any]: 27 | """ 28 | Waits for subtasks to complete. 29 | 30 | This function is used by mapper 31 | step. 32 | 33 | It awaits for all tasks from task_ids 34 | to complete and then collects results 35 | in single list. 36 | 37 | :param task_ids: list of task ids. 38 | :param check_interval: how often task completions are checked. 39 | :param context: current execution context, defaults to default_context 40 | :param skip_errors: doesn't fail pipeline if error is found. 41 | :raises TaskiqError: if error is found and skip_erros is false. 42 | :return: list of results. 43 | """ 44 | ordered_ids = task_ids[:] 45 | tasks_set = set(task_ids) 46 | while tasks_set: 47 | for task_id in task_ids: 48 | if await context.broker.result_backend.is_result_ready(task_id): 49 | try: 50 | tasks_set.remove(task_id) 51 | except LookupError: 52 | continue 53 | if tasks_set: 54 | await asyncio.sleep(check_interval) 55 | 56 | results = [] 57 | for task_id in ordered_ids: 58 | result = await context.broker.result_backend.get_result(task_id) 59 | if result.is_err: 60 | if skip_errors: 61 | continue 62 | err_cause = None 63 | if isinstance(result.error, BaseException): 64 | err_cause = result.error 65 | raise MappingError(task_id=task_id, error=result.error) from err_cause 66 | 67 | results.append(result.return_value) 68 | return results 69 | 70 | 71 | class MapperStep(pydantic.BaseModel, AbstractStep, step_name="mapper"): 72 | """Step that maps iterables.""" 73 | 74 | task_name: str 75 | labels: Dict[str, str] 76 | param_name: Optional[str] 77 | additional_kwargs: Dict[str, Any] 78 | skip_errors: bool 79 | check_interval: float 80 | 81 | async def act( 82 | self, 83 | broker: AsyncBroker, 84 | step_number: int, 85 | parent_task_id: str, 86 | task_id: str, 87 | pipe_data: str, 88 | result: "TaskiqResult[Any]", 89 | ) -> None: 90 | """ 91 | Runs mapping. 92 | 93 | This step creates many small 94 | tasks and one waiter task. 95 | 96 | The waiter task awaits 97 | for all small tasks to complete, 98 | and then assembles the final result. 99 | 100 | :param broker: current broker. 101 | :param step_number: current step number. 102 | :param task_id: waiter task_id. 103 | :param parent_task_id: task_id of the previous step. 104 | :param pipe_data: serialized pipeline. 105 | :param result: result of the previous task. 106 | :raises AbortPipeline: if the result of the 107 | previous task is not iterable. 108 | """ 109 | sub_task_ids: List[str] = [] 110 | return_value = result.return_value 111 | if not isinstance(return_value, Iterable): 112 | raise AbortPipeline(reason="Result of the previous task is not iterable.") 113 | 114 | for item in return_value: 115 | kicker: "AsyncKicker[Any, Any]" = AsyncKicker( 116 | task_name=self.task_name, 117 | broker=broker, 118 | labels=self.labels, 119 | ) 120 | if self.param_name: 121 | self.additional_kwargs[self.param_name] = item 122 | task = await kicker.kiq(**self.additional_kwargs) 123 | else: 124 | task = await kicker.kiq(item, **self.additional_kwargs) 125 | sub_task_ids.append(task.task_id) 126 | 127 | await ( 128 | wait_tasks.kicker() 129 | .with_task_id(task_id) 130 | .with_broker( 131 | broker, 132 | ) 133 | .with_labels( 134 | **{CURRENT_STEP: step_number, PIPELINE_DATA: pipe_data}, # type: ignore 135 | ) 136 | .kiq( 137 | sub_task_ids, 138 | check_interval=self.check_interval, 139 | skip_errors=self.skip_errors, 140 | ) 141 | ) 142 | 143 | @classmethod 144 | def from_task( 145 | cls, 146 | task: Union[ 147 | AsyncKicker[Any, Any], 148 | AsyncTaskiqDecoratedTask[Any, Any], 149 | ], 150 | param_name: Optional[str], 151 | skip_errors: bool, 152 | check_interval: float, 153 | **additional_kwargs: Any, 154 | ) -> "MapperStep": 155 | """ 156 | Create new mapper step from task. 157 | 158 | :param task: task to execute. 159 | :param param_name: parameter name. 160 | :param skip_errors: don't fail collector 161 | task on errors. 162 | :param check_interval: how often tasks are checked. 163 | :param additional_kwargs: additional function's kwargs. 164 | :return: new mapper step. 165 | """ 166 | kicker = task.kicker() if isinstance(task, AsyncTaskiqDecoratedTask) else task 167 | message = kicker._prepare_message() 168 | return MapperStep( 169 | task_name=message.task_name, 170 | labels=message.labels, 171 | param_name=param_name, 172 | additional_kwargs=additional_kwargs, 173 | skip_errors=skip_errors, 174 | check_interval=check_interval, 175 | ) 176 | -------------------------------------------------------------------------------- /taskiq_pipelines/steps/sequential.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | import pydantic 4 | from taskiq import AsyncBroker, AsyncTaskiqDecoratedTask, TaskiqResult 5 | from taskiq.kicker import AsyncKicker 6 | 7 | from taskiq_pipelines.abc import AbstractStep 8 | from taskiq_pipelines.constants import CURRENT_STEP, EMPTY_PARAM_NAME, PIPELINE_DATA 9 | 10 | 11 | class SequentialStep(pydantic.BaseModel, AbstractStep, step_name="sequential"): 12 | """ 13 | Step that's simply runs next function. 14 | 15 | It passes the result of the previous function 16 | as the first argument or as the keyword argument, 17 | if param_name is specified. 18 | """ 19 | 20 | task_name: str 21 | labels: Dict[str, str] 22 | # order is important here, otherwise pydantic will always choose str. 23 | # we use int instead of Literal[-1] because pydantic thinks that -1 is always str. 24 | param_name: Union[Optional[int], str] 25 | additional_kwargs: Dict[str, Any] 26 | 27 | async def act( 28 | self, 29 | broker: AsyncBroker, 30 | step_number: int, 31 | parent_task_id: str, 32 | task_id: str, 33 | pipe_data: str, 34 | result: "TaskiqResult[Any]", 35 | ) -> None: 36 | """ 37 | Runs next task. 38 | 39 | This step is simple. 40 | 41 | It creates new task and passes the result of 42 | the previous task as the first argument. 43 | 44 | Or it may pass it as key word argument, 45 | if param_name is not None. 46 | 47 | :param broker: current broker. 48 | :param step_number: current step number. 49 | :param parent_task_id: current step's task id. 50 | :param task_id: new task id. 51 | :param pipe_data: serialized pipeline. 52 | :param result: result of the previous task. 53 | """ 54 | kicker: "AsyncKicker[Any, Any]" = ( 55 | AsyncKicker( 56 | task_name=self.task_name, 57 | broker=broker, 58 | labels=self.labels, 59 | ) 60 | .with_task_id(task_id) 61 | .with_labels( 62 | **{PIPELINE_DATA: pipe_data, CURRENT_STEP: step_number}, # type: ignore 63 | ) 64 | ) 65 | if isinstance(self.param_name, str): 66 | self.additional_kwargs[self.param_name] = result.return_value 67 | await kicker.kiq(**self.additional_kwargs) 68 | elif self.param_name == EMPTY_PARAM_NAME: 69 | await kicker.kiq(**self.additional_kwargs) 70 | else: 71 | await kicker.kiq(result.return_value, **self.additional_kwargs) 72 | 73 | @classmethod 74 | def from_task( 75 | cls, 76 | task: Union[ 77 | AsyncKicker[Any, Any], 78 | AsyncTaskiqDecoratedTask[Any, Any], 79 | ], 80 | param_name: Union[Optional[str], int], 81 | **additional_kwargs: Any, 82 | ) -> "SequentialStep": 83 | """ 84 | Create step from given task. 85 | 86 | Also this method takes additional 87 | parameters. 88 | 89 | :param task: task to call. 90 | :param param_name: parameter name, defaults to None. 91 | :param additional_kwargs: additional kwargs to task. 92 | :return: new sequential step. 93 | """ 94 | kicker = task.kicker() if isinstance(task, AsyncTaskiqDecoratedTask) else task 95 | message = kicker._prepare_message() 96 | return SequentialStep( 97 | task_name=message.task_name, 98 | labels=message.labels, 99 | param_name=param_name, 100 | additional_kwargs=additional_kwargs, 101 | ) 102 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="session") 5 | def anyio_backend() -> str: 6 | """ 7 | Anyio backend. 8 | 9 | Backend for anyio pytest plugin. 10 | :return: backend name. 11 | """ 12 | return "asyncio" 13 | -------------------------------------------------------------------------------- /tests/test_steps.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | from taskiq import InMemoryBroker 5 | 6 | from taskiq_pipelines import AbortPipeline, Pipeline, PipelineMiddleware 7 | 8 | 9 | @pytest.mark.anyio 10 | async def test_success() -> None: 11 | """Tests that sequential step works as expected.""" 12 | broker = InMemoryBroker().with_middlewares(PipelineMiddleware()) 13 | 14 | @broker.task 15 | def add(i: int) -> int: 16 | return i + 1 17 | 18 | @broker.task 19 | def double(i: int) -> int: 20 | return i * 2 21 | 22 | pipe = Pipeline(broker, add).call_next(double) 23 | sent = await pipe.kiq(1) 24 | res = await sent.wait_result() 25 | assert res.return_value == 4 26 | 27 | 28 | @pytest.mark.anyio 29 | async def test_mapping_success() -> None: 30 | """Test that map step works as expected.""" 31 | broker = InMemoryBroker().with_middlewares(PipelineMiddleware()) 32 | 33 | @broker.task 34 | def ranger(i: int) -> List[int]: 35 | return list(range(i)) 36 | 37 | @broker.task 38 | def double(i: int) -> int: 39 | return i * 2 40 | 41 | pipe = Pipeline(broker, ranger).map(double) 42 | sent = await pipe.kiq(4) 43 | res = await sent.wait_result() 44 | assert res.return_value == list(map(double, ranger(4))) 45 | 46 | 47 | @pytest.mark.anyio 48 | async def test_abort_pipeline() -> None: 49 | """Test AbortPipeline.""" 50 | broker = InMemoryBroker().with_middlewares(PipelineMiddleware()) 51 | text = "task was aborted" 52 | 53 | @broker.task 54 | def normal_task(i: bool) -> bool: 55 | return i 56 | 57 | @broker.task 58 | def aborting_task(i: int) -> bool: 59 | if i: 60 | raise AbortPipeline(reason=text) 61 | return True 62 | 63 | pipe = Pipeline(broker, aborting_task).call_next(normal_task) 64 | sent = await pipe.kiq(0) 65 | res = await sent.wait_result() 66 | assert res.is_err is False 67 | assert res.return_value is True 68 | assert res.error is None 69 | sent = await pipe.kiq(1) 70 | res = await sent.wait_result() 71 | assert res.is_err is True 72 | assert res.return_value is None 73 | assert text in res.error.args[0] 74 | --------------------------------------------------------------------------------