diff --git a/.github/.github/pull_request_template.md b/.github/.github/pull_request_template.md new file mode 100644 index 0000000..44cc820 --- /dev/null +++ b/.github/.github/pull_request_template.md @@ -0,0 +1,4 @@ +- [ ] Does this PR have impact on local development experience? If yes, make sure you have a plan and add the documentations to address issues that come with the change +- [ ] bump version +- [ ] make a release +- [ ] publish to pypi service diff --git a/.github/.github/workflows/pre-commit.yml b/.github/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..73b29eb --- /dev/null +++ b/.github/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - uses: pre-commit/action@v3.0.0 diff --git a/.github/.github/workflows/pytest.yml b/.github/.github/workflows/pytest.yml new file mode 100644 index 0000000..a512fd0 --- /dev/null +++ b/.github/.github/workflows/pytest.yml @@ -0,0 +1,73 @@ +name: pytest + +# Run this job on pushes to `main`, and for pull requests. If you don't specify +# `branches: [main], then this actions runs _twice_ on pull requests, which is +# annoying. +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + # If you wanted to use multiple Python versions, you'd have specify a matrix in the job and + # reference the matrixe python version here. + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + # Cache the installation of Poetry itself, e.g. the next step. This prevents the workflow + # from installing Poetry every time, which can be slow. Note the use of the Poetry version + # number in the cache key, and the "-0" suffix: this allows you to invalidate the cache + # manually if/when you want to upgrade Poetry, or if something goes wrong. This could be + # mildly cleaner by using an environment variable, but I don't really care. + - name: cache poetry install + uses: actions/cache@v3 + with: + path: ~/.local + key: poetry-1.2.2-0 + + # Install Poetry. You could do this manually, or there are several actions that do this. + # `snok/install-poetry` seems to be minimal yet complete, and really just calls out to + # Poetry's default install script, which feels correct. I pin the Poetry version here + # because Poetry does occasionally change APIs between versions and I don't want my + # actions to break if it does. + # + # The key configuration value here is `virtualenvs-in-project: true`: this creates the + # venv as a `.venv` in your testing directory, which allows the next step to easily + # cache it. + - uses: snok/install-poetry@v1 + with: + version: 1.2.2 + virtualenvs-create: true + virtualenvs-in-project: true + + # Cache your dependencies (i.e. all the stuff in your `pyproject.toml`). Note the cache + # key: if you're using multiple Python versions, or multiple OSes, you'd need to include + # them in the cache key. I'm not, so it can be simple and just depend on the poetry.lock. + - name: cache deps + id: cache-deps + uses: actions/cache@v3 + with: + path: .venv + key: pydeps-${{ hashFiles('**/poetry.lock') }} + + # Install dependencies. `--no-root` means "install all dependencies but not the project + # itself", which is what you want to avoid caching _your_ code. The `if` statement + # ensures this only runs on a cache miss. + - run: poetry install --no-interaction --no-root + if: steps.cache-deps.outputs.cache-hit != 'true' + + # Now install _your_ project. This isn't necessary for many types of projects -- particularly + # things like Django apps don't need this. But it's a good idea since it fully-exercises the + # pyproject.toml and makes that if you add things like console-scripts at some point that + # they'll be installed and working. + - run: poetry install --no-interaction + + # And finally run tests. I'm using pytest and all my pytest config is in my `pyproject.toml` + # so this line is super-simple. But it could be as complex as you need. + - run: poetry run pytest diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..44cc820 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,4 @@ +- [ ] Does this PR have impact on local development experience? If yes, make sure you have a plan and add the documentations to address issues that come with the change +- [ ] bump version +- [ ] make a release +- [ ] publish to pypi service diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..73b29eb --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..a512fd0 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,73 @@ +name: pytest + +# Run this job on pushes to `main`, and for pull requests. If you don't specify +# `branches: [main], then this actions runs _twice_ on pull requests, which is +# annoying. +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + # If you wanted to use multiple Python versions, you'd have specify a matrix in the job and + # reference the matrixe python version here. + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + # Cache the installation of Poetry itself, e.g. the next step. This prevents the workflow + # from installing Poetry every time, which can be slow. Note the use of the Poetry version + # number in the cache key, and the "-0" suffix: this allows you to invalidate the cache + # manually if/when you want to upgrade Poetry, or if something goes wrong. This could be + # mildly cleaner by using an environment variable, but I don't really care. + - name: cache poetry install + uses: actions/cache@v3 + with: + path: ~/.local + key: poetry-1.2.2-0 + + # Install Poetry. You could do this manually, or there are several actions that do this. + # `snok/install-poetry` seems to be minimal yet complete, and really just calls out to + # Poetry's default install script, which feels correct. I pin the Poetry version here + # because Poetry does occasionally change APIs between versions and I don't want my + # actions to break if it does. + # + # The key configuration value here is `virtualenvs-in-project: true`: this creates the + # venv as a `.venv` in your testing directory, which allows the next step to easily + # cache it. + - uses: snok/install-poetry@v1 + with: + version: 1.2.2 + virtualenvs-create: true + virtualenvs-in-project: true + + # Cache your dependencies (i.e. all the stuff in your `pyproject.toml`). Note the cache + # key: if you're using multiple Python versions, or multiple OSes, you'd need to include + # them in the cache key. I'm not, so it can be simple and just depend on the poetry.lock. + - name: cache deps + id: cache-deps + uses: actions/cache@v3 + with: + path: .venv + key: pydeps-${{ hashFiles('**/poetry.lock') }} + + # Install dependencies. `--no-root` means "install all dependencies but not the project + # itself", which is what you want to avoid caching _your_ code. The `if` statement + # ensures this only runs on a cache miss. + - run: poetry install --no-interaction --no-root + if: steps.cache-deps.outputs.cache-hit != 'true' + + # Now install _your_ project. This isn't necessary for many types of projects -- particularly + # things like Django apps don't need this. But it's a good idea since it fully-exercises the + # pyproject.toml and makes that if you add things like console-scripts at some point that + # they'll be installed and working. + - run: poetry install --no-interaction + + # And finally run tests. I'm using pytest and all my pytest config is in my `pyproject.toml` + # so this line is super-simple. But it could be as complex as you need. + - run: poetry run pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b89296 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# PyCharm +.idea/ +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# ctags +tags + +# mac +.DS_Store + +# vim +*.swp + +# pyright +pyrightconfig.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a1f94e2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,112 @@ +repos: + # Standard hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + - id: fix-encoding-pragma + + # Black, the code formatter, natively supports pre-commit + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + + - repo: https://github.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear, pep8-naming] + + - repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.0 + hooks: + - id: autopep8 + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.0.0 + hooks: + - id: mypy + args: [--show-error-codes, --python-version=3.10] + additional_dependencies: + - fastapi + - uvicorn + - typer + - types-pyyaml + - types-boto3 + - pyhumps + - setuptools + - pandas-stubs + - types-redis + - python-dotenv + - feast + - pytest + - types-protobuf + - snowflake-connector-python + - fastapi-utils + - pyinstrument + - ddtrace + - msgspec + - lz4 + - types-requests + - more-itertools + - tqdm + - types-tqdm + - nest-asyncio + exclude: "^tests/" + + # Check for spelling + - repo: https://github.com/codespell-project/codespell + rev: v2.1.0 + hooks: + - id: codespell + exclude: ".supp$" + args: ["-L", "nd,ot,thist,paramater"] + + - repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.1 + hooks: + - id: add-trailing-comma + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-check-blanket-noqa + - id: python-check-mock-methods + - id: python-no-log-warn + - id: python-use-type-annotations + + - repo: https://github.com/prettier/pre-commit + rev: 57f39166b5a5a504d6808b87ab98d41ebf095b46 + hooks: + - id: prettier + + # Disallow some common capitalization mistakes + - repo: local + hooks: + - id: disallow-caps + name: Disallow improper capitalization + language: pygrep + entry: PyBind|Numpy|Cmake|CCache|PyTest|PyTest-Cov + exclude: | + (?x)( + ^\.pre-commit-config.yaml$ | + poetry.lock$ + ) + - repo: https://github.com/thlorenz/doctoc + rev: v2.2.0 + hooks: + - id: doctoc + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) diff --git a/.testing_env b/.testing_env new file mode 100644 index 0000000..9549679 --- /dev/null +++ b/.testing_env @@ -0,0 +1 @@ +DD_TRACE_ENABLED=false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..954054c --- /dev/null +++ b/LICENSE @@ -0,0 +1,46 @@ +Artie Transfer uses Elastic License 2.0 + +Elastic License 2.0 (ELv2) + +**Acceptance** +By using the software, you agree to all of the terms and conditions below. + +**Copyright License** +The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below + +**Limitations** +You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software. + +You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key. + +You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. + +**Patents** +The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +**Notices** +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software. + +**No Other Rights** +These terms do not imply any licenses other than those expressly granted in these terms. + +**Termination** +If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. + +**No Liability** +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. + +**Definitions** +The *licensor* is the entity offering these terms, and the *software* is the software the licensor makes available under these terms, including any portion of it. + +*you* refers to the individual or entity agreeing to these terms. + +*your company* is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. *control* means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +*your licenses* are all the licenses granted to you for the software under these terms. + +*use* means anything you do with the software requiring one of your licenses. + +*trademark* means trademarks, service marks, and similar rights. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ffc6ab --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ + + + +- [Setup](#setup) + - [0. Set up PyEnv](#0-set-up-pyenv) + - [1. Set up Poetry env](#1-set-up-poetry-env) + - [1. Install Poetry](#1-install-poetry) + - [2. Set up Poetry to create virtual envs in the local directory](#2-set-up-poetry-to-create-virtual-envs-in-the-local-directory) + - [3. Python Version](#3-python-version) + - [4. Set up the virtual environment](#4-set-up-the-virtual-environment) + - [(Optional) 5. Set up auto-poetry shell spawning](#optional-5-set-up-auto-poetry-shell-spawning) + - [3. Set up Pre-commit](#3-set-up-pre-commit) +- [Development](#development) + + + +# Setup + +This project is managed by Poetry + +## 0. Set up PyEnv + +Install PyEnv using Brew: + +```bash +brew install pyenv +``` + +Add this to your ~/.zshrc or ~/.bashrc depending on what you use. Documentation copied from [here](https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv) + +```bash + export PYENV_ROOT="$HOME/.pyenv" + command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" +``` + +## 1. Set up Poetry env + +### 1. Install Poetry + +```bash +brew install poetry +``` + +### 2. Set up Poetry to create virtual envs in the local directory + +```bash +poetry config virtualenvs.in-project true +``` + +### 3. Python Version + +Poetry apparently has trouble initializing the Python version itself, so you'll have to force it to use the correct version + +At the time of this writing, the correct version is 3.10, so just run: + +``` +poetry env use 3.10 +``` + +And it'll switch the python version to the correct one. You only need to do this once + +### 4. Set up the virtual environment + +Have poetry set up all of the configs + +```bash +poetry install +``` + +### (Optional) 5. Set up auto-poetry shell spawning + +Add this to your ~/.zshrc: + +This automatically spawns a new poetry shell whenever you `cd` into a directory with a poetry env + +```bash +### Autoomatically activate virtual environment +function auto_poetry_shell { + if [ -f "pyproject.toml" ] ; then + source ./.venv/bin/activate + fi +} + +function cd { + builtin cd "$@" + auto_poetry_shell +} + +auto_poetry_shell +``` + +## 3. Set up Pre-commit + +```bash +brew install pre-commit +pre-commit install +``` + +# Development + +To work with the development version of this codebase in an external branch, you will need to run: + +```bazaar +pip install -e ../path/to/wyvern-framework + + +example: If you're working in another project and you have this repo checked out +pip install -e ../wyvern-framework +``` diff --git a/docs/deploy-diagram.png b/docs/deploy-diagram.png new file mode 100644 index 0000000..4fa7756 Binary files /dev/null and b/docs/deploy-diagram.png differ diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/example_business_logic.py b/examples/example_business_logic.py new file mode 100644 index 0000000..366f781 --- /dev/null +++ b/examples/example_business_logic.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +import asyncio +import json +import logging +from typing import List + +from pydantic import BaseModel + +from wyvern.components.business_logic.boosting_business_logic import ( + BoostingBusinessLogicComponent, +) +from wyvern.components.business_logic.business_logic import ( + BusinessLogicEvent, + BusinessLogicPipeline, + BusinessLogicRequest, +) +from wyvern.components.component import Component +from wyvern.entities.candidate_entities import CandidateSetEntity, ScoredCandidate +from wyvern.entities.identifier_entities import ProductEntity, QueryEntity +from wyvern.entities.request import BaseWyvernRequest + +logger = logging.getLogger(__name__) + + +class SimpleProductEntity(ProductEntity): + product_name: str = "" + product_description: str = "" + + +class ExampleProductSearchRankingRequest( + BaseWyvernRequest, + QueryEntity, + CandidateSetEntity[SimpleProductEntity], +): + pass + + +class CandleBoostingBusinessLogicComponent( + BoostingBusinessLogicComponent[ + SimpleProductEntity, + ExampleProductSearchRankingRequest, + ], +): + async def execute( + self, + input: BusinessLogicRequest[ + SimpleProductEntity, + ExampleProductSearchRankingRequest, + ], + **kwargs, + ) -> List[ScoredCandidate[SimpleProductEntity]]: + # TODO (suchintan): Feature request: Add a way to load a CSV from S3 + # Define this in a CSV (query, entity_keys, boost) + # Get access to S3 + # Load CSV from S3 + # Reference the file and boost it -- reload the file every 15 minutes or something like that + logger.info(f"Boosting candles for query={input.request.query}") + if input.request.query == "candle": + return self.boost(input.scored_candidates, entity_keys={"3"}, boost=100) + else: + return input.scored_candidates + + +class AlwaysBoostWaxSealProduct( + BoostingBusinessLogicComponent[ + SimpleProductEntity, + ExampleProductSearchRankingRequest, + ], +): + async def execute( + self, + input: BusinessLogicRequest[ + SimpleProductEntity, + ExampleProductSearchRankingRequest, + ], + **kwargs, + ) -> List[ScoredCandidate[SimpleProductEntity]]: + return self.boost(input.scored_candidates, entity_keys={"7"}, boost=100) + + +class SearchBusinessLogicPipeline( + BusinessLogicPipeline[SimpleProductEntity, ExampleProductSearchRankingRequest], +): + def __init__(self): + super().__init__( + CandleBoostingBusinessLogicComponent(), + AlwaysBoostWaxSealProduct(), + name="search_business_logic_pipeline", + ) + + +search_business_logic_pipeline = SearchBusinessLogicPipeline() + + +class ExampleProductSearchRankingCandidateResponse(BaseModel): + product_name: str + old_rank: int + old_score: float + new_rank: int + new_score: float + + +class ExampleProductSearchRankingResponse(BaseModel): + ranked_products: List[ExampleProductSearchRankingCandidateResponse] + events: List[BusinessLogicEvent] + + +class ProductQueryRankingBusinessLogicComponent( + Component[ExampleProductSearchRankingRequest, ExampleProductSearchRankingResponse], +): + def __init__(self): + super().__init__( + search_business_logic_pipeline, + name="product_query_ranking_business_logic_component", + ) + + async def execute( + self, input: ExampleProductSearchRankingRequest, **kwargs + ) -> ExampleProductSearchRankingResponse: + logger.info(f"Input request: {input}") + # Set up a really silly score + scored_candidates: List[ScoredCandidate] = [ + ScoredCandidate(entity=candidate, score=(len(input.candidates) - i)) + for i, candidate in enumerate(input.candidates) + ] + + business_logic_request = BusinessLogicRequest[ + SimpleProductEntity, + ExampleProductSearchRankingRequest, + ]( + request=input, + scored_candidates=scored_candidates, + ) + + ranked_products = await search_business_logic_pipeline.execute( + business_logic_request, + ) + + pretty_ranked_products = [ + ExampleProductSearchRankingCandidateResponse( + product_name=entity_score.entity.product_name, + old_rank=input.candidates.index(entity_score.entity), + old_score=ranked_products.request.scored_candidates[ + input.candidates.index(entity_score.entity) + ].score, + new_rank=i, + new_score=entity_score.score, + ) + for i, entity_score in enumerate(ranked_products.adjusted_candidates) + ] + + return ExampleProductSearchRankingResponse( + ranked_products=pretty_ranked_products, + ) + + +def create_example_business_logic_component() -> ProductQueryRankingBusinessLogicComponent: + business_logic_component = ProductQueryRankingBusinessLogicComponent() + return business_logic_component + + +async def sample_product_query_ranking_request() -> None: + """ + How to run this: `python wyvern/examples/example_business_logic.py` + + Json representation of the request: + { + "request_id": "rrr", + "query": "candle", + "candidates": [ + {"product_id": "1", "product_name": "scented candle"}, + {"product_id": "2", "product_name": "hot candle"}, + {"product_id": "3", "product_name": "pumpkin candle"}, + {"product_id": "4", "product_name": "unrelated item"}, + {"product_id": "5", "product_name": "candle holder accessory"}, + {"product_id": "6", "product_name": "earwax holder"}, + {"product_id": "7", "product_name": "wax seal"} + ], + } + """ + logger.info("Start query product business logic case...") + req = ExampleProductSearchRankingRequest( + request_id="rrr", + query="candle", + candidates=[ + SimpleProductEntity(product_id="1", product_name="scented candle"), + SimpleProductEntity(product_id="2", product_name="hot candle"), + SimpleProductEntity(product_id="3", product_name="pumpkin candle"), + SimpleProductEntity(product_id="4", product_name="unrelated item"), + SimpleProductEntity(product_id="5", product_name="candle holder accessory"), + SimpleProductEntity(product_id="6", product_name="earwax holder"), + SimpleProductEntity(product_id="7", product_name="wax seal"), + ], + ) + + component = create_example_business_logic_component() + await component.initialize() + + response = await component.execute(req) + + json_formatted_str = json.dumps( + response.dict(), + indent=2, + ) + logger.info(f"Response: {json_formatted_str}") + + +if __name__ == "__main__": + asyncio.run(sample_product_query_ranking_request()) diff --git a/examples/feature_store_main.py b/examples/feature_store_main.py new file mode 100644 index 0000000..fed0ff3 --- /dev/null +++ b/examples/feature_store_main.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +import logging +from typing import Dict + +import typer +from pydantic import BaseModel + +from wyvern.components.api_route_component import APIRouteComponent +from wyvern.components.features.feature_store import ( + FeatureStoreRetrievalRequest, + feature_store_retrieval_component, +) +from wyvern.entities.feature_entities import FeatureData +from wyvern.service import WyvernService + +wyvern_cli_app = typer.Typer() + +logger = logging.getLogger(__name__) + + +class FeatureStoreResponse(BaseModel): + feature_data: Dict[str, FeatureData] + + +class FeatureStoreTestingComponent( + APIRouteComponent[FeatureStoreRetrievalRequest, FeatureStoreResponse], +): + PATH = "/feature-store-testing" + REQUEST_SCHEMA_CLASS = FeatureStoreRetrievalRequest + RESPONSE_SCHEMA_CLASS = FeatureStoreResponse + + async def execute( + self, input: FeatureStoreRetrievalRequest, **kwargs + ) -> FeatureStoreResponse: + logger.info(f"Executing input {input}") + feature_map = await feature_store_retrieval_component.execute(input) + + return FeatureStoreResponse( + feature_data={ + identifier.identifier: feature_map.feature_map[identifier] + for identifier in feature_map.feature_map.keys() + }, + ) + + +@wyvern_cli_app.command() +def run( + host: str = "127.0.0.1", + port: int = 8000, +) -> None: + """ + Run your wyvern service + """ + WyvernService.run( + route_components=[FeatureStoreTestingComponent], + host=host, + port=port, + ) + + +if __name__ == "__main__": + # TODO (suchintan): Add support for hot swapping code here + wyvern_cli_app() diff --git a/examples/real_time_features_main.py b/examples/real_time_features_main.py new file mode 100644 index 0000000..a52b284 --- /dev/null +++ b/examples/real_time_features_main.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +import logging +from time import time +from typing import Any, Dict, List, Optional + +import typer +from pydantic import BaseModel +from pyinstrument import Profiler + +from wyvern.components.api_route_component import APIRouteComponent +from wyvern.components.features.feature_retrieval_pipeline import ( + FeatureRetrievalPipeline, + FeatureRetrievalPipelineRequest, +) +from wyvern.components.features.realtime_features_component import ( + RealtimeFeatureComponent, + RealtimeFeatureRequest, +) +from wyvern.entities.candidate_entities import CandidateSetEntity +from wyvern.entities.feature_entities import FeatureData +from wyvern.entities.identifier import ( + CompositeIdentifier, + Identifier, + SimpleIdentifierType, +) +from wyvern.entities.identifier_entities import ProductEntity, QueryEntity, UserEntity +from wyvern.entities.request import BaseWyvernRequest +from wyvern.service import WyvernService +from wyvern.wyvern_typing import WyvernFeature + +wyvern_cli_app = typer.Typer() + +logger = logging.getLogger(__name__) + + +class Product(ProductEntity): + product_id: str + opensearch_score: Optional[float] + candidate_order: Optional[int] + matched_queries: List[str] = [] + + +class Query(QueryEntity): + query: str + + +class User(UserEntity): + user_id: str + user_name: str + + +class FeatureStoreRequest( + BaseWyvernRequest, + CandidateSetEntity[Product], +): + request_id: str + query: Query + candidates: List[Product] + user: Optional[User] + + +class FeatureStoreResponse(BaseModel): + feature_data: Dict[str, FeatureData] + + +class RealTimeProductFeature(RealtimeFeatureComponent[Product, Any, Any]): + def __init__(self): + super().__init__( + output_feature_names={"f_opensearch_score"}, + ) + + async def compute_features( + self, + entity: Product, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=entity.identifier, + features={ + "f_opensearch_score": entity.opensearch_score, + }, + ) + + +class RealTimeMatchedQueriesProductFeature( + RealtimeFeatureComponent[Product, Query, Any], +): + def __init__(self): + super().__init__( + output_feature_names=set(), + ) + + async def compute_composite_features( + self, + primary_entity: Product, + secondary_entity: Query, + request: Any, + ) -> Optional[FeatureData]: + features: Dict[str, WyvernFeature] = { + f"f_matched_queries_{query}": 1.0 + for query in primary_entity.matched_queries + } + + return FeatureData( + identifier=CompositeIdentifier( + primary_entity.identifier, + secondary_entity.identifier, + ), + features=features, + ) + + +class RealTimeNumberOfCandidatesFeature( + RealtimeFeatureComponent[Any, Any, FeatureStoreRequest], +): + def __init__(self): + super().__init__( + output_feature_names={"f_number_of_candidates"}, + ) + + async def compute_request_features( + self, + request: RealtimeFeatureRequest[FeatureStoreRequest], + ) -> Optional[FeatureData]: + + return FeatureData( + identifier=Identifier( + identifier=request.request.request_id, + identifier_type=SimpleIdentifierType.REQUEST, + ), + features={ + "f_number_of_candidates": len(request.request.candidates), + }, + ) + + +class RealTimeQueryFeature(RealtimeFeatureComponent[Query, Any, Any]): + def __init__(self): + super().__init__( + output_feature_names={"f_query_length"}, + ) + + async def compute_features( + self, + entity: Query, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=entity.identifier, + features={ + "f_query_length": len(entity.query), + }, + ) + + +class RealTimeStringFeature(RealtimeFeatureComponent[Query, Any, Any]): + def __init__(self): + super().__init__( + output_feature_names={"f_query"}, + ) + + async def compute_features( + self, + entity: Query, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=entity.identifier, + features={ + "f_query": entity.query, + }, + ) + + +class RealTimeEmbeddingFeature(RealtimeFeatureComponent[Query, Any, Any]): + def __init__(self): + super().__init__( + output_feature_names={"f_query_embedding_vector_8"}, + ) + + async def compute_features( + self, + entity: Query, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=entity.identifier, + features={"f_query_embedding_vector_8": [1, 2, 3, 4, 5, 6, 7, 8]}, + ) + + +class RealTimeQueryProductFeature(RealtimeFeatureComponent[Product, Query, Any]): + def __init__(self): + super().__init__( + output_feature_names={ + "f_query_product_name_edit_distance", + "f_query_product_name_jaccard_similarity", + }, + ) + + async def compute_composite_features( + self, + primary_entity: Product, + secondary_entity: Query, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=CompositeIdentifier( + primary_identifier=primary_entity.identifier, + secondary_identifier=secondary_entity.identifier, + ), + features={ + "f_query_product_name_edit_distance": len(secondary_entity.query) + - len(primary_entity.product_id), + "f_query_product_name_jaccard_similarity": len( + primary_entity.product_id, + ) + - len(secondary_entity.query), + }, + ) + + +class RealTimeUserFeature(RealtimeFeatureComponent[User, Any, Any]): + def __init__(self): + super().__init__( + output_feature_names={"f_user_name_length"}, + ) + + async def compute_features( + self, + entity: User, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=entity.identifier, + features={ + "f_user_name_length": len(entity.user_name), + }, + ) + + +class RealTimeUserProductFeature(RealtimeFeatureComponent[Product, User, Any]): + def __init__(self): + super().__init__( + output_feature_names={ + "f_user_product_name_edit_distance", + "f_user_product_name_jaccard_similarity", + }, + ) + + async def compute_composite_features( + self, + primary_entity: Product, + secondary_entity: User, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=CompositeIdentifier( + primary_identifier=primary_entity.identifier, + secondary_identifier=secondary_entity.identifier, + ), + features={ + "f_user_product_name_edit_distance": len(secondary_entity.user_name) + - len(primary_entity.product_id), + "f_user_product_name_jaccard_similarity": len(primary_entity.product_id) + - len(secondary_entity.user_name), + }, + ) + + +class RealTimeUserQueryFeature(RealtimeFeatureComponent[Query, User, Any]): + def __init__(self): + super().__init__( + output_feature_names={ + "f_user_query_name_edit_distance", + "f_user_query_name_jaccard_similarity", + }, + ) + + async def compute_composite_features( + self, + primary_entity: Query, + secondary_entity: User, + request: Any, + ) -> Optional[FeatureData]: + return FeatureData( + identifier=CompositeIdentifier( + primary_identifier=primary_entity.identifier, + secondary_identifier=secondary_entity.identifier, + ), + features={ + "f_user_query_name_edit_distance": len(secondary_entity.user_name) + - len(primary_entity.query), + "f_user_query_name_jaccard_similarity": len(primary_entity.query) + - len(secondary_entity.user_name), + }, + ) + + +class RealTimeFeatureTestingComponent( + APIRouteComponent[FeatureStoreRequest, FeatureStoreResponse], +): + PATH = "/real-time-features-testing" + REQUEST_SCHEMA_CLASS = FeatureStoreRequest + RESPONSE_SCHEMA_CLASS = FeatureStoreResponse + + def __init__(self): + self.feature_retrieval_pipeline = FeatureRetrievalPipeline( + name="real-time-features-testing", + ) + super().__init__(self.feature_retrieval_pipeline) + + async def execute( + self, input: FeatureStoreRequest, **kwargs + ) -> FeatureStoreResponse: + profiler = Profiler() + profiler.start() + + # TODO (suchintan) -- actually request some features here + request = FeatureRetrievalPipelineRequest[FeatureStoreRequest]( + request=input, + requested_feature_names={ + "RealTimeProductFeature:f_opensearch_score", + "RealTimeNumberOfCandidatesFeature:f_number_of_candidates", + "RealTimeQueryFeature:f_query_length", + "RealTimeQueryProductFeature:f_query_product_name_edit_distance", + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity", + "RealTimeUserFeature:f_user_name_length", + "RealTimeUserProductFeature:f_user_product_name_edit_distance", + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity", + "RealTimeUserQueryFeature:f_user_query_name_edit_distance", + "RealTimeUserQueryFeature:f_user_query_name_jaccard_similarity", + "RealTimeStringFeature:f_query", + "RealTimeEmbeddingFeature:f_query_embedding_vector_8", + }, + feature_overrides={RealTimeMatchedQueriesProductFeature}, + ) + + time_start = time() + feature_map = await self.feature_retrieval_pipeline.execute(request) + logger.info(f"operation feature_retrieval took:{time()-time_start:2.4f} sec") + profiler.stop() + profiler.print() + return FeatureStoreResponse( + feature_data={ + str(identifier): feature_map.feature_map[identifier] + for identifier in feature_map.feature_map.keys() + }, + ) + + +@wyvern_cli_app.command() +def run( + host: str = "127.0.0.1", + port: int = 8000, +) -> None: + """ + Run your wyvern service + """ + WyvernService.run( + route_components=[RealTimeFeatureTestingComponent], + host=host, + port=port, + ) + + +if __name__ == "__main__": + wyvern_cli_app() diff --git a/log_config.yml b/log_config.yml new file mode 100644 index 0000000..d6fdf9b --- /dev/null +++ b/log_config.yml @@ -0,0 +1,14 @@ +version: 1 +disable_existing_loggers: False +formatters: + timestamped: + format: "%(levelname)s: [%(asctime)s] [%(name)s] %(message)s" +handlers: + console: + class: logging.StreamHandler + formatter: timestamped + level: INFO + stream: ext://sys.stdout +root: + level: INFO + handlers: [console] diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..dc7b863 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,4432 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "3.7.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +optional = false +python-versions = "*" +files = [ + {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, + {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, +] + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bleach" +version = "6.0.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.7" +files = [ + {file = "bleach-6.0.0-py3-none-any.whl", hash = "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"}, + {file = "bleach-6.0.0.tar.gz", hash = "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414"}, +] + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.2)"] + +[[package]] +name = "boto3" +version = "1.28.20" +description = "The AWS SDK for Python" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "boto3-1.28.20-py3-none-any.whl", hash = "sha256:a654d57e3882e7fd2c1260d604a44364a2fed00da4f52faf37e5901e71145df1"}, + {file = "boto3-1.28.20.tar.gz", hash = "sha256:e3c2e8e55c17af6671a5332d6ab4635ad9793c80d0ac6d78af7b30a994d0681b"}, +] + +[package.dependencies] +botocore = ">=1.31.20,<1.32.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "boto3-stubs" +version = "1.28.20" +description = "Type annotations for boto3 1.28.20 generated with mypy-boto3-builder 7.17.2" +optional = false +python-versions = ">=3.7" +files = [ + {file = "boto3-stubs-1.28.20.tar.gz", hash = "sha256:21050c5dc37a8f34dfe7ed8b1b12d1fd9ff187eaf4bd9bfa187585e07e6b6b76"}, + {file = "boto3_stubs-1.28.20-py3-none-any.whl", hash = "sha256:e08cc66bf744e12cbdcbcd64ab520900ee5fc61c22bceefe08a2f04cdb1d5c0c"}, +] + +[package.dependencies] +botocore-stubs = "*" +types-s3transfer = "*" +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.9\""} + +[package.extras] +accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.28.0,<1.29.0)"] +account = ["mypy-boto3-account (>=1.28.0,<1.29.0)"] +acm = ["mypy-boto3-acm (>=1.28.0,<1.29.0)"] +acm-pca = ["mypy-boto3-acm-pca (>=1.28.0,<1.29.0)"] +alexaforbusiness = ["mypy-boto3-alexaforbusiness (>=1.28.0,<1.29.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.28.0,<1.29.0)", "mypy-boto3-account (>=1.28.0,<1.29.0)", "mypy-boto3-acm (>=1.28.0,<1.29.0)", "mypy-boto3-acm-pca (>=1.28.0,<1.29.0)", "mypy-boto3-alexaforbusiness (>=1.28.0,<1.29.0)", "mypy-boto3-amp (>=1.28.0,<1.29.0)", "mypy-boto3-amplify (>=1.28.0,<1.29.0)", "mypy-boto3-amplifybackend (>=1.28.0,<1.29.0)", "mypy-boto3-amplifyuibuilder (>=1.28.0,<1.29.0)", "mypy-boto3-apigateway (>=1.28.0,<1.29.0)", "mypy-boto3-apigatewaymanagementapi (>=1.28.0,<1.29.0)", "mypy-boto3-apigatewayv2 (>=1.28.0,<1.29.0)", "mypy-boto3-appconfig (>=1.28.0,<1.29.0)", "mypy-boto3-appconfigdata (>=1.28.0,<1.29.0)", "mypy-boto3-appfabric (>=1.28.0,<1.29.0)", "mypy-boto3-appflow (>=1.28.0,<1.29.0)", "mypy-boto3-appintegrations (>=1.28.0,<1.29.0)", "mypy-boto3-application-autoscaling (>=1.28.0,<1.29.0)", "mypy-boto3-application-insights (>=1.28.0,<1.29.0)", "mypy-boto3-applicationcostprofiler (>=1.28.0,<1.29.0)", "mypy-boto3-appmesh (>=1.28.0,<1.29.0)", "mypy-boto3-apprunner (>=1.28.0,<1.29.0)", "mypy-boto3-appstream (>=1.28.0,<1.29.0)", "mypy-boto3-appsync (>=1.28.0,<1.29.0)", "mypy-boto3-arc-zonal-shift (>=1.28.0,<1.29.0)", "mypy-boto3-athena (>=1.28.0,<1.29.0)", "mypy-boto3-auditmanager (>=1.28.0,<1.29.0)", "mypy-boto3-autoscaling (>=1.28.0,<1.29.0)", "mypy-boto3-autoscaling-plans (>=1.28.0,<1.29.0)", "mypy-boto3-backup (>=1.28.0,<1.29.0)", "mypy-boto3-backup-gateway (>=1.28.0,<1.29.0)", "mypy-boto3-backupstorage (>=1.28.0,<1.29.0)", "mypy-boto3-batch (>=1.28.0,<1.29.0)", "mypy-boto3-billingconductor (>=1.28.0,<1.29.0)", "mypy-boto3-braket (>=1.28.0,<1.29.0)", "mypy-boto3-budgets (>=1.28.0,<1.29.0)", "mypy-boto3-ce (>=1.28.0,<1.29.0)", "mypy-boto3-chime (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-identity (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-meetings (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-messaging (>=1.28.0,<1.29.0)", "mypy-boto3-chime-sdk-voice (>=1.28.0,<1.29.0)", "mypy-boto3-cleanrooms (>=1.28.0,<1.29.0)", "mypy-boto3-cloud9 (>=1.28.0,<1.29.0)", "mypy-boto3-cloudcontrol (>=1.28.0,<1.29.0)", "mypy-boto3-clouddirectory (>=1.28.0,<1.29.0)", "mypy-boto3-cloudformation (>=1.28.0,<1.29.0)", "mypy-boto3-cloudfront (>=1.28.0,<1.29.0)", "mypy-boto3-cloudhsm (>=1.28.0,<1.29.0)", "mypy-boto3-cloudhsmv2 (>=1.28.0,<1.29.0)", "mypy-boto3-cloudsearch (>=1.28.0,<1.29.0)", "mypy-boto3-cloudsearchdomain (>=1.28.0,<1.29.0)", "mypy-boto3-cloudtrail (>=1.28.0,<1.29.0)", "mypy-boto3-cloudtrail-data (>=1.28.0,<1.29.0)", "mypy-boto3-cloudwatch (>=1.28.0,<1.29.0)", "mypy-boto3-codeartifact (>=1.28.0,<1.29.0)", "mypy-boto3-codebuild (>=1.28.0,<1.29.0)", "mypy-boto3-codecatalyst (>=1.28.0,<1.29.0)", "mypy-boto3-codecommit (>=1.28.0,<1.29.0)", "mypy-boto3-codedeploy (>=1.28.0,<1.29.0)", "mypy-boto3-codeguru-reviewer (>=1.28.0,<1.29.0)", "mypy-boto3-codeguru-security (>=1.28.0,<1.29.0)", "mypy-boto3-codeguruprofiler (>=1.28.0,<1.29.0)", "mypy-boto3-codepipeline (>=1.28.0,<1.29.0)", "mypy-boto3-codestar (>=1.28.0,<1.29.0)", "mypy-boto3-codestar-connections (>=1.28.0,<1.29.0)", "mypy-boto3-codestar-notifications (>=1.28.0,<1.29.0)", "mypy-boto3-cognito-identity (>=1.28.0,<1.29.0)", "mypy-boto3-cognito-idp (>=1.28.0,<1.29.0)", "mypy-boto3-cognito-sync (>=1.28.0,<1.29.0)", "mypy-boto3-comprehend (>=1.28.0,<1.29.0)", "mypy-boto3-comprehendmedical (>=1.28.0,<1.29.0)", "mypy-boto3-compute-optimizer (>=1.28.0,<1.29.0)", "mypy-boto3-config (>=1.28.0,<1.29.0)", "mypy-boto3-connect (>=1.28.0,<1.29.0)", "mypy-boto3-connect-contact-lens (>=1.28.0,<1.29.0)", "mypy-boto3-connectcampaigns (>=1.28.0,<1.29.0)", "mypy-boto3-connectcases (>=1.28.0,<1.29.0)", "mypy-boto3-connectparticipant (>=1.28.0,<1.29.0)", "mypy-boto3-controltower (>=1.28.0,<1.29.0)", "mypy-boto3-cur (>=1.28.0,<1.29.0)", "mypy-boto3-customer-profiles (>=1.28.0,<1.29.0)", "mypy-boto3-databrew (>=1.28.0,<1.29.0)", "mypy-boto3-dataexchange (>=1.28.0,<1.29.0)", "mypy-boto3-datapipeline (>=1.28.0,<1.29.0)", "mypy-boto3-datasync (>=1.28.0,<1.29.0)", "mypy-boto3-dax (>=1.28.0,<1.29.0)", "mypy-boto3-detective (>=1.28.0,<1.29.0)", "mypy-boto3-devicefarm (>=1.28.0,<1.29.0)", "mypy-boto3-devops-guru (>=1.28.0,<1.29.0)", "mypy-boto3-directconnect (>=1.28.0,<1.29.0)", "mypy-boto3-discovery (>=1.28.0,<1.29.0)", "mypy-boto3-dlm (>=1.28.0,<1.29.0)", "mypy-boto3-dms (>=1.28.0,<1.29.0)", "mypy-boto3-docdb (>=1.28.0,<1.29.0)", "mypy-boto3-docdb-elastic (>=1.28.0,<1.29.0)", "mypy-boto3-drs (>=1.28.0,<1.29.0)", "mypy-boto3-ds (>=1.28.0,<1.29.0)", "mypy-boto3-dynamodb (>=1.28.0,<1.29.0)", "mypy-boto3-dynamodbstreams (>=1.28.0,<1.29.0)", "mypy-boto3-ebs (>=1.28.0,<1.29.0)", "mypy-boto3-ec2 (>=1.28.0,<1.29.0)", "mypy-boto3-ec2-instance-connect (>=1.28.0,<1.29.0)", "mypy-boto3-ecr (>=1.28.0,<1.29.0)", "mypy-boto3-ecr-public (>=1.28.0,<1.29.0)", "mypy-boto3-ecs (>=1.28.0,<1.29.0)", "mypy-boto3-efs (>=1.28.0,<1.29.0)", "mypy-boto3-eks (>=1.28.0,<1.29.0)", "mypy-boto3-elastic-inference (>=1.28.0,<1.29.0)", "mypy-boto3-elasticache (>=1.28.0,<1.29.0)", "mypy-boto3-elasticbeanstalk (>=1.28.0,<1.29.0)", "mypy-boto3-elastictranscoder (>=1.28.0,<1.29.0)", "mypy-boto3-elb (>=1.28.0,<1.29.0)", "mypy-boto3-elbv2 (>=1.28.0,<1.29.0)", "mypy-boto3-emr (>=1.28.0,<1.29.0)", "mypy-boto3-emr-containers (>=1.28.0,<1.29.0)", "mypy-boto3-emr-serverless (>=1.28.0,<1.29.0)", "mypy-boto3-entityresolution (>=1.28.0,<1.29.0)", "mypy-boto3-es (>=1.28.0,<1.29.0)", "mypy-boto3-events (>=1.28.0,<1.29.0)", "mypy-boto3-evidently (>=1.28.0,<1.29.0)", "mypy-boto3-finspace (>=1.28.0,<1.29.0)", "mypy-boto3-finspace-data (>=1.28.0,<1.29.0)", "mypy-boto3-firehose (>=1.28.0,<1.29.0)", "mypy-boto3-fis (>=1.28.0,<1.29.0)", "mypy-boto3-fms (>=1.28.0,<1.29.0)", "mypy-boto3-forecast (>=1.28.0,<1.29.0)", "mypy-boto3-forecastquery (>=1.28.0,<1.29.0)", "mypy-boto3-frauddetector (>=1.28.0,<1.29.0)", "mypy-boto3-fsx (>=1.28.0,<1.29.0)", "mypy-boto3-gamelift (>=1.28.0,<1.29.0)", "mypy-boto3-gamesparks (>=1.28.0,<1.29.0)", "mypy-boto3-glacier (>=1.28.0,<1.29.0)", "mypy-boto3-globalaccelerator (>=1.28.0,<1.29.0)", "mypy-boto3-glue (>=1.28.0,<1.29.0)", "mypy-boto3-grafana (>=1.28.0,<1.29.0)", "mypy-boto3-greengrass (>=1.28.0,<1.29.0)", "mypy-boto3-greengrassv2 (>=1.28.0,<1.29.0)", "mypy-boto3-groundstation (>=1.28.0,<1.29.0)", "mypy-boto3-guardduty (>=1.28.0,<1.29.0)", "mypy-boto3-health (>=1.28.0,<1.29.0)", "mypy-boto3-healthlake (>=1.28.0,<1.29.0)", "mypy-boto3-honeycode (>=1.28.0,<1.29.0)", "mypy-boto3-iam (>=1.28.0,<1.29.0)", "mypy-boto3-identitystore (>=1.28.0,<1.29.0)", "mypy-boto3-imagebuilder (>=1.28.0,<1.29.0)", "mypy-boto3-importexport (>=1.28.0,<1.29.0)", "mypy-boto3-inspector (>=1.28.0,<1.29.0)", "mypy-boto3-inspector2 (>=1.28.0,<1.29.0)", "mypy-boto3-internetmonitor (>=1.28.0,<1.29.0)", "mypy-boto3-iot (>=1.28.0,<1.29.0)", "mypy-boto3-iot-data (>=1.28.0,<1.29.0)", "mypy-boto3-iot-jobs-data (>=1.28.0,<1.29.0)", "mypy-boto3-iot-roborunner (>=1.28.0,<1.29.0)", "mypy-boto3-iot1click-devices (>=1.28.0,<1.29.0)", "mypy-boto3-iot1click-projects (>=1.28.0,<1.29.0)", "mypy-boto3-iotanalytics (>=1.28.0,<1.29.0)", "mypy-boto3-iotdeviceadvisor (>=1.28.0,<1.29.0)", "mypy-boto3-iotevents (>=1.28.0,<1.29.0)", "mypy-boto3-iotevents-data (>=1.28.0,<1.29.0)", "mypy-boto3-iotfleethub (>=1.28.0,<1.29.0)", "mypy-boto3-iotfleetwise (>=1.28.0,<1.29.0)", "mypy-boto3-iotsecuretunneling (>=1.28.0,<1.29.0)", "mypy-boto3-iotsitewise (>=1.28.0,<1.29.0)", "mypy-boto3-iotthingsgraph (>=1.28.0,<1.29.0)", "mypy-boto3-iottwinmaker (>=1.28.0,<1.29.0)", "mypy-boto3-iotwireless (>=1.28.0,<1.29.0)", "mypy-boto3-ivs (>=1.28.0,<1.29.0)", "mypy-boto3-ivs-realtime (>=1.28.0,<1.29.0)", "mypy-boto3-ivschat (>=1.28.0,<1.29.0)", "mypy-boto3-kafka (>=1.28.0,<1.29.0)", "mypy-boto3-kafkaconnect (>=1.28.0,<1.29.0)", "mypy-boto3-kendra (>=1.28.0,<1.29.0)", "mypy-boto3-kendra-ranking (>=1.28.0,<1.29.0)", "mypy-boto3-keyspaces (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-archived-media (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-media (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-signaling (>=1.28.0,<1.29.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.28.0,<1.29.0)", "mypy-boto3-kinesisanalytics (>=1.28.0,<1.29.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.28.0,<1.29.0)", "mypy-boto3-kinesisvideo (>=1.28.0,<1.29.0)", "mypy-boto3-kms (>=1.28.0,<1.29.0)", "mypy-boto3-lakeformation (>=1.28.0,<1.29.0)", "mypy-boto3-lambda (>=1.28.0,<1.29.0)", "mypy-boto3-lex-models (>=1.28.0,<1.29.0)", "mypy-boto3-lex-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-lexv2-models (>=1.28.0,<1.29.0)", "mypy-boto3-lexv2-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-license-manager (>=1.28.0,<1.29.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.28.0,<1.29.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.28.0,<1.29.0)", "mypy-boto3-lightsail (>=1.28.0,<1.29.0)", "mypy-boto3-location (>=1.28.0,<1.29.0)", "mypy-boto3-logs (>=1.28.0,<1.29.0)", "mypy-boto3-lookoutequipment (>=1.28.0,<1.29.0)", "mypy-boto3-lookoutmetrics (>=1.28.0,<1.29.0)", "mypy-boto3-lookoutvision (>=1.28.0,<1.29.0)", "mypy-boto3-m2 (>=1.28.0,<1.29.0)", "mypy-boto3-machinelearning (>=1.28.0,<1.29.0)", "mypy-boto3-macie (>=1.28.0,<1.29.0)", "mypy-boto3-macie2 (>=1.28.0,<1.29.0)", "mypy-boto3-managedblockchain (>=1.28.0,<1.29.0)", "mypy-boto3-managedblockchain-query (>=1.28.0,<1.29.0)", "mypy-boto3-marketplace-catalog (>=1.28.0,<1.29.0)", "mypy-boto3-marketplace-entitlement (>=1.28.0,<1.29.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.28.0,<1.29.0)", "mypy-boto3-mediaconnect (>=1.28.0,<1.29.0)", "mypy-boto3-mediaconvert (>=1.28.0,<1.29.0)", "mypy-boto3-medialive (>=1.28.0,<1.29.0)", "mypy-boto3-mediapackage (>=1.28.0,<1.29.0)", "mypy-boto3-mediapackage-vod (>=1.28.0,<1.29.0)", "mypy-boto3-mediapackagev2 (>=1.28.0,<1.29.0)", "mypy-boto3-mediastore (>=1.28.0,<1.29.0)", "mypy-boto3-mediastore-data (>=1.28.0,<1.29.0)", "mypy-boto3-mediatailor (>=1.28.0,<1.29.0)", "mypy-boto3-medical-imaging (>=1.28.0,<1.29.0)", "mypy-boto3-memorydb (>=1.28.0,<1.29.0)", "mypy-boto3-meteringmarketplace (>=1.28.0,<1.29.0)", "mypy-boto3-mgh (>=1.28.0,<1.29.0)", "mypy-boto3-mgn (>=1.28.0,<1.29.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.28.0,<1.29.0)", "mypy-boto3-migrationhub-config (>=1.28.0,<1.29.0)", "mypy-boto3-migrationhuborchestrator (>=1.28.0,<1.29.0)", "mypy-boto3-migrationhubstrategy (>=1.28.0,<1.29.0)", "mypy-boto3-mobile (>=1.28.0,<1.29.0)", "mypy-boto3-mq (>=1.28.0,<1.29.0)", "mypy-boto3-mturk (>=1.28.0,<1.29.0)", "mypy-boto3-mwaa (>=1.28.0,<1.29.0)", "mypy-boto3-neptune (>=1.28.0,<1.29.0)", "mypy-boto3-network-firewall (>=1.28.0,<1.29.0)", "mypy-boto3-networkmanager (>=1.28.0,<1.29.0)", "mypy-boto3-nimble (>=1.28.0,<1.29.0)", "mypy-boto3-oam (>=1.28.0,<1.29.0)", "mypy-boto3-omics (>=1.28.0,<1.29.0)", "mypy-boto3-opensearch (>=1.28.0,<1.29.0)", "mypy-boto3-opensearchserverless (>=1.28.0,<1.29.0)", "mypy-boto3-opsworks (>=1.28.0,<1.29.0)", "mypy-boto3-opsworkscm (>=1.28.0,<1.29.0)", "mypy-boto3-organizations (>=1.28.0,<1.29.0)", "mypy-boto3-osis (>=1.28.0,<1.29.0)", "mypy-boto3-outposts (>=1.28.0,<1.29.0)", "mypy-boto3-panorama (>=1.28.0,<1.29.0)", "mypy-boto3-payment-cryptography (>=1.28.0,<1.29.0)", "mypy-boto3-payment-cryptography-data (>=1.28.0,<1.29.0)", "mypy-boto3-personalize (>=1.28.0,<1.29.0)", "mypy-boto3-personalize-events (>=1.28.0,<1.29.0)", "mypy-boto3-personalize-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-pi (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint-email (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint-sms-voice (>=1.28.0,<1.29.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.28.0,<1.29.0)", "mypy-boto3-pipes (>=1.28.0,<1.29.0)", "mypy-boto3-polly (>=1.28.0,<1.29.0)", "mypy-boto3-pricing (>=1.28.0,<1.29.0)", "mypy-boto3-privatenetworks (>=1.28.0,<1.29.0)", "mypy-boto3-proton (>=1.28.0,<1.29.0)", "mypy-boto3-qldb (>=1.28.0,<1.29.0)", "mypy-boto3-qldb-session (>=1.28.0,<1.29.0)", "mypy-boto3-quicksight (>=1.28.0,<1.29.0)", "mypy-boto3-ram (>=1.28.0,<1.29.0)", "mypy-boto3-rbin (>=1.28.0,<1.29.0)", "mypy-boto3-rds (>=1.28.0,<1.29.0)", "mypy-boto3-rds-data (>=1.28.0,<1.29.0)", "mypy-boto3-redshift (>=1.28.0,<1.29.0)", "mypy-boto3-redshift-data (>=1.28.0,<1.29.0)", "mypy-boto3-redshift-serverless (>=1.28.0,<1.29.0)", "mypy-boto3-rekognition (>=1.28.0,<1.29.0)", "mypy-boto3-resiliencehub (>=1.28.0,<1.29.0)", "mypy-boto3-resource-explorer-2 (>=1.28.0,<1.29.0)", "mypy-boto3-resource-groups (>=1.28.0,<1.29.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.28.0,<1.29.0)", "mypy-boto3-robomaker (>=1.28.0,<1.29.0)", "mypy-boto3-rolesanywhere (>=1.28.0,<1.29.0)", "mypy-boto3-route53 (>=1.28.0,<1.29.0)", "mypy-boto3-route53-recovery-cluster (>=1.28.0,<1.29.0)", "mypy-boto3-route53-recovery-control-config (>=1.28.0,<1.29.0)", "mypy-boto3-route53-recovery-readiness (>=1.28.0,<1.29.0)", "mypy-boto3-route53domains (>=1.28.0,<1.29.0)", "mypy-boto3-route53resolver (>=1.28.0,<1.29.0)", "mypy-boto3-rum (>=1.28.0,<1.29.0)", "mypy-boto3-s3 (>=1.28.0,<1.29.0)", "mypy-boto3-s3control (>=1.28.0,<1.29.0)", "mypy-boto3-s3outposts (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-edge (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-geospatial (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-metrics (>=1.28.0,<1.29.0)", "mypy-boto3-sagemaker-runtime (>=1.28.0,<1.29.0)", "mypy-boto3-savingsplans (>=1.28.0,<1.29.0)", "mypy-boto3-scheduler (>=1.28.0,<1.29.0)", "mypy-boto3-schemas (>=1.28.0,<1.29.0)", "mypy-boto3-sdb (>=1.28.0,<1.29.0)", "mypy-boto3-secretsmanager (>=1.28.0,<1.29.0)", "mypy-boto3-securityhub (>=1.28.0,<1.29.0)", "mypy-boto3-securitylake (>=1.28.0,<1.29.0)", "mypy-boto3-serverlessrepo (>=1.28.0,<1.29.0)", "mypy-boto3-service-quotas (>=1.28.0,<1.29.0)", "mypy-boto3-servicecatalog (>=1.28.0,<1.29.0)", "mypy-boto3-servicecatalog-appregistry (>=1.28.0,<1.29.0)", "mypy-boto3-servicediscovery (>=1.28.0,<1.29.0)", "mypy-boto3-ses (>=1.28.0,<1.29.0)", "mypy-boto3-sesv2 (>=1.28.0,<1.29.0)", "mypy-boto3-shield (>=1.28.0,<1.29.0)", "mypy-boto3-signer (>=1.28.0,<1.29.0)", "mypy-boto3-simspaceweaver (>=1.28.0,<1.29.0)", "mypy-boto3-sms (>=1.28.0,<1.29.0)", "mypy-boto3-sms-voice (>=1.28.0,<1.29.0)", "mypy-boto3-snow-device-management (>=1.28.0,<1.29.0)", "mypy-boto3-snowball (>=1.28.0,<1.29.0)", "mypy-boto3-sns (>=1.28.0,<1.29.0)", "mypy-boto3-sqs (>=1.28.0,<1.29.0)", "mypy-boto3-ssm (>=1.28.0,<1.29.0)", "mypy-boto3-ssm-contacts (>=1.28.0,<1.29.0)", "mypy-boto3-ssm-incidents (>=1.28.0,<1.29.0)", "mypy-boto3-ssm-sap (>=1.28.0,<1.29.0)", "mypy-boto3-sso (>=1.28.0,<1.29.0)", "mypy-boto3-sso-admin (>=1.28.0,<1.29.0)", "mypy-boto3-sso-oidc (>=1.28.0,<1.29.0)", "mypy-boto3-stepfunctions (>=1.28.0,<1.29.0)", "mypy-boto3-storagegateway (>=1.28.0,<1.29.0)", "mypy-boto3-sts (>=1.28.0,<1.29.0)", "mypy-boto3-support (>=1.28.0,<1.29.0)", "mypy-boto3-support-app (>=1.28.0,<1.29.0)", "mypy-boto3-swf (>=1.28.0,<1.29.0)", "mypy-boto3-synthetics (>=1.28.0,<1.29.0)", "mypy-boto3-textract (>=1.28.0,<1.29.0)", "mypy-boto3-timestream-query (>=1.28.0,<1.29.0)", "mypy-boto3-timestream-write (>=1.28.0,<1.29.0)", "mypy-boto3-tnb (>=1.28.0,<1.29.0)", "mypy-boto3-transcribe (>=1.28.0,<1.29.0)", "mypy-boto3-transfer (>=1.28.0,<1.29.0)", "mypy-boto3-translate (>=1.28.0,<1.29.0)", "mypy-boto3-verifiedpermissions (>=1.28.0,<1.29.0)", "mypy-boto3-voice-id (>=1.28.0,<1.29.0)", "mypy-boto3-vpc-lattice (>=1.28.0,<1.29.0)", "mypy-boto3-waf (>=1.28.0,<1.29.0)", "mypy-boto3-waf-regional (>=1.28.0,<1.29.0)", "mypy-boto3-wafv2 (>=1.28.0,<1.29.0)", "mypy-boto3-wellarchitected (>=1.28.0,<1.29.0)", "mypy-boto3-wisdom (>=1.28.0,<1.29.0)", "mypy-boto3-workdocs (>=1.28.0,<1.29.0)", "mypy-boto3-worklink (>=1.28.0,<1.29.0)", "mypy-boto3-workmail (>=1.28.0,<1.29.0)", "mypy-boto3-workmailmessageflow (>=1.28.0,<1.29.0)", "mypy-boto3-workspaces (>=1.28.0,<1.29.0)", "mypy-boto3-workspaces-web (>=1.28.0,<1.29.0)", "mypy-boto3-xray (>=1.28.0,<1.29.0)"] +amp = ["mypy-boto3-amp (>=1.28.0,<1.29.0)"] +amplify = ["mypy-boto3-amplify (>=1.28.0,<1.29.0)"] +amplifybackend = ["mypy-boto3-amplifybackend (>=1.28.0,<1.29.0)"] +amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.28.0,<1.29.0)"] +apigateway = ["mypy-boto3-apigateway (>=1.28.0,<1.29.0)"] +apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.28.0,<1.29.0)"] +apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.28.0,<1.29.0)"] +appconfig = ["mypy-boto3-appconfig (>=1.28.0,<1.29.0)"] +appconfigdata = ["mypy-boto3-appconfigdata (>=1.28.0,<1.29.0)"] +appfabric = ["mypy-boto3-appfabric (>=1.28.0,<1.29.0)"] +appflow = ["mypy-boto3-appflow (>=1.28.0,<1.29.0)"] +appintegrations = ["mypy-boto3-appintegrations (>=1.28.0,<1.29.0)"] +application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.28.0,<1.29.0)"] +application-insights = ["mypy-boto3-application-insights (>=1.28.0,<1.29.0)"] +applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.28.0,<1.29.0)"] +appmesh = ["mypy-boto3-appmesh (>=1.28.0,<1.29.0)"] +apprunner = ["mypy-boto3-apprunner (>=1.28.0,<1.29.0)"] +appstream = ["mypy-boto3-appstream (>=1.28.0,<1.29.0)"] +appsync = ["mypy-boto3-appsync (>=1.28.0,<1.29.0)"] +arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.28.0,<1.29.0)"] +athena = ["mypy-boto3-athena (>=1.28.0,<1.29.0)"] +auditmanager = ["mypy-boto3-auditmanager (>=1.28.0,<1.29.0)"] +autoscaling = ["mypy-boto3-autoscaling (>=1.28.0,<1.29.0)"] +autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.28.0,<1.29.0)"] +backup = ["mypy-boto3-backup (>=1.28.0,<1.29.0)"] +backup-gateway = ["mypy-boto3-backup-gateway (>=1.28.0,<1.29.0)"] +backupstorage = ["mypy-boto3-backupstorage (>=1.28.0,<1.29.0)"] +batch = ["mypy-boto3-batch (>=1.28.0,<1.29.0)"] +billingconductor = ["mypy-boto3-billingconductor (>=1.28.0,<1.29.0)"] +boto3 = ["boto3 (==1.28.20)", "botocore (==1.31.20)"] +braket = ["mypy-boto3-braket (>=1.28.0,<1.29.0)"] +budgets = ["mypy-boto3-budgets (>=1.28.0,<1.29.0)"] +ce = ["mypy-boto3-ce (>=1.28.0,<1.29.0)"] +chime = ["mypy-boto3-chime (>=1.28.0,<1.29.0)"] +chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.28.0,<1.29.0)"] +chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.28.0,<1.29.0)"] +chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.28.0,<1.29.0)"] +chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.28.0,<1.29.0)"] +chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.28.0,<1.29.0)"] +cleanrooms = ["mypy-boto3-cleanrooms (>=1.28.0,<1.29.0)"] +cloud9 = ["mypy-boto3-cloud9 (>=1.28.0,<1.29.0)"] +cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.28.0,<1.29.0)"] +clouddirectory = ["mypy-boto3-clouddirectory (>=1.28.0,<1.29.0)"] +cloudformation = ["mypy-boto3-cloudformation (>=1.28.0,<1.29.0)"] +cloudfront = ["mypy-boto3-cloudfront (>=1.28.0,<1.29.0)"] +cloudhsm = ["mypy-boto3-cloudhsm (>=1.28.0,<1.29.0)"] +cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.28.0,<1.29.0)"] +cloudsearch = ["mypy-boto3-cloudsearch (>=1.28.0,<1.29.0)"] +cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.28.0,<1.29.0)"] +cloudtrail = ["mypy-boto3-cloudtrail (>=1.28.0,<1.29.0)"] +cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.28.0,<1.29.0)"] +cloudwatch = ["mypy-boto3-cloudwatch (>=1.28.0,<1.29.0)"] +codeartifact = ["mypy-boto3-codeartifact (>=1.28.0,<1.29.0)"] +codebuild = ["mypy-boto3-codebuild (>=1.28.0,<1.29.0)"] +codecatalyst = ["mypy-boto3-codecatalyst (>=1.28.0,<1.29.0)"] +codecommit = ["mypy-boto3-codecommit (>=1.28.0,<1.29.0)"] +codedeploy = ["mypy-boto3-codedeploy (>=1.28.0,<1.29.0)"] +codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.28.0,<1.29.0)"] +codeguru-security = ["mypy-boto3-codeguru-security (>=1.28.0,<1.29.0)"] +codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.28.0,<1.29.0)"] +codepipeline = ["mypy-boto3-codepipeline (>=1.28.0,<1.29.0)"] +codestar = ["mypy-boto3-codestar (>=1.28.0,<1.29.0)"] +codestar-connections = ["mypy-boto3-codestar-connections (>=1.28.0,<1.29.0)"] +codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.28.0,<1.29.0)"] +cognito-identity = ["mypy-boto3-cognito-identity (>=1.28.0,<1.29.0)"] +cognito-idp = ["mypy-boto3-cognito-idp (>=1.28.0,<1.29.0)"] +cognito-sync = ["mypy-boto3-cognito-sync (>=1.28.0,<1.29.0)"] +comprehend = ["mypy-boto3-comprehend (>=1.28.0,<1.29.0)"] +comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.28.0,<1.29.0)"] +compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.28.0,<1.29.0)"] +config = ["mypy-boto3-config (>=1.28.0,<1.29.0)"] +connect = ["mypy-boto3-connect (>=1.28.0,<1.29.0)"] +connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.28.0,<1.29.0)"] +connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.28.0,<1.29.0)"] +connectcases = ["mypy-boto3-connectcases (>=1.28.0,<1.29.0)"] +connectparticipant = ["mypy-boto3-connectparticipant (>=1.28.0,<1.29.0)"] +controltower = ["mypy-boto3-controltower (>=1.28.0,<1.29.0)"] +cur = ["mypy-boto3-cur (>=1.28.0,<1.29.0)"] +customer-profiles = ["mypy-boto3-customer-profiles (>=1.28.0,<1.29.0)"] +databrew = ["mypy-boto3-databrew (>=1.28.0,<1.29.0)"] +dataexchange = ["mypy-boto3-dataexchange (>=1.28.0,<1.29.0)"] +datapipeline = ["mypy-boto3-datapipeline (>=1.28.0,<1.29.0)"] +datasync = ["mypy-boto3-datasync (>=1.28.0,<1.29.0)"] +dax = ["mypy-boto3-dax (>=1.28.0,<1.29.0)"] +detective = ["mypy-boto3-detective (>=1.28.0,<1.29.0)"] +devicefarm = ["mypy-boto3-devicefarm (>=1.28.0,<1.29.0)"] +devops-guru = ["mypy-boto3-devops-guru (>=1.28.0,<1.29.0)"] +directconnect = ["mypy-boto3-directconnect (>=1.28.0,<1.29.0)"] +discovery = ["mypy-boto3-discovery (>=1.28.0,<1.29.0)"] +dlm = ["mypy-boto3-dlm (>=1.28.0,<1.29.0)"] +dms = ["mypy-boto3-dms (>=1.28.0,<1.29.0)"] +docdb = ["mypy-boto3-docdb (>=1.28.0,<1.29.0)"] +docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.28.0,<1.29.0)"] +drs = ["mypy-boto3-drs (>=1.28.0,<1.29.0)"] +ds = ["mypy-boto3-ds (>=1.28.0,<1.29.0)"] +dynamodb = ["mypy-boto3-dynamodb (>=1.28.0,<1.29.0)"] +dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.28.0,<1.29.0)"] +ebs = ["mypy-boto3-ebs (>=1.28.0,<1.29.0)"] +ec2 = ["mypy-boto3-ec2 (>=1.28.0,<1.29.0)"] +ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.28.0,<1.29.0)"] +ecr = ["mypy-boto3-ecr (>=1.28.0,<1.29.0)"] +ecr-public = ["mypy-boto3-ecr-public (>=1.28.0,<1.29.0)"] +ecs = ["mypy-boto3-ecs (>=1.28.0,<1.29.0)"] +efs = ["mypy-boto3-efs (>=1.28.0,<1.29.0)"] +eks = ["mypy-boto3-eks (>=1.28.0,<1.29.0)"] +elastic-inference = ["mypy-boto3-elastic-inference (>=1.28.0,<1.29.0)"] +elasticache = ["mypy-boto3-elasticache (>=1.28.0,<1.29.0)"] +elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.28.0,<1.29.0)"] +elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.28.0,<1.29.0)"] +elb = ["mypy-boto3-elb (>=1.28.0,<1.29.0)"] +elbv2 = ["mypy-boto3-elbv2 (>=1.28.0,<1.29.0)"] +emr = ["mypy-boto3-emr (>=1.28.0,<1.29.0)"] +emr-containers = ["mypy-boto3-emr-containers (>=1.28.0,<1.29.0)"] +emr-serverless = ["mypy-boto3-emr-serverless (>=1.28.0,<1.29.0)"] +entityresolution = ["mypy-boto3-entityresolution (>=1.28.0,<1.29.0)"] +es = ["mypy-boto3-es (>=1.28.0,<1.29.0)"] +essential = ["mypy-boto3-cloudformation (>=1.28.0,<1.29.0)", "mypy-boto3-dynamodb (>=1.28.0,<1.29.0)", "mypy-boto3-ec2 (>=1.28.0,<1.29.0)", "mypy-boto3-lambda (>=1.28.0,<1.29.0)", "mypy-boto3-rds (>=1.28.0,<1.29.0)", "mypy-boto3-s3 (>=1.28.0,<1.29.0)", "mypy-boto3-sqs (>=1.28.0,<1.29.0)"] +events = ["mypy-boto3-events (>=1.28.0,<1.29.0)"] +evidently = ["mypy-boto3-evidently (>=1.28.0,<1.29.0)"] +finspace = ["mypy-boto3-finspace (>=1.28.0,<1.29.0)"] +finspace-data = ["mypy-boto3-finspace-data (>=1.28.0,<1.29.0)"] +firehose = ["mypy-boto3-firehose (>=1.28.0,<1.29.0)"] +fis = ["mypy-boto3-fis (>=1.28.0,<1.29.0)"] +fms = ["mypy-boto3-fms (>=1.28.0,<1.29.0)"] +forecast = ["mypy-boto3-forecast (>=1.28.0,<1.29.0)"] +forecastquery = ["mypy-boto3-forecastquery (>=1.28.0,<1.29.0)"] +frauddetector = ["mypy-boto3-frauddetector (>=1.28.0,<1.29.0)"] +fsx = ["mypy-boto3-fsx (>=1.28.0,<1.29.0)"] +gamelift = ["mypy-boto3-gamelift (>=1.28.0,<1.29.0)"] +gamesparks = ["mypy-boto3-gamesparks (>=1.28.0,<1.29.0)"] +glacier = ["mypy-boto3-glacier (>=1.28.0,<1.29.0)"] +globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.28.0,<1.29.0)"] +glue = ["mypy-boto3-glue (>=1.28.0,<1.29.0)"] +grafana = ["mypy-boto3-grafana (>=1.28.0,<1.29.0)"] +greengrass = ["mypy-boto3-greengrass (>=1.28.0,<1.29.0)"] +greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.28.0,<1.29.0)"] +groundstation = ["mypy-boto3-groundstation (>=1.28.0,<1.29.0)"] +guardduty = ["mypy-boto3-guardduty (>=1.28.0,<1.29.0)"] +health = ["mypy-boto3-health (>=1.28.0,<1.29.0)"] +healthlake = ["mypy-boto3-healthlake (>=1.28.0,<1.29.0)"] +honeycode = ["mypy-boto3-honeycode (>=1.28.0,<1.29.0)"] +iam = ["mypy-boto3-iam (>=1.28.0,<1.29.0)"] +identitystore = ["mypy-boto3-identitystore (>=1.28.0,<1.29.0)"] +imagebuilder = ["mypy-boto3-imagebuilder (>=1.28.0,<1.29.0)"] +importexport = ["mypy-boto3-importexport (>=1.28.0,<1.29.0)"] +inspector = ["mypy-boto3-inspector (>=1.28.0,<1.29.0)"] +inspector2 = ["mypy-boto3-inspector2 (>=1.28.0,<1.29.0)"] +internetmonitor = ["mypy-boto3-internetmonitor (>=1.28.0,<1.29.0)"] +iot = ["mypy-boto3-iot (>=1.28.0,<1.29.0)"] +iot-data = ["mypy-boto3-iot-data (>=1.28.0,<1.29.0)"] +iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.28.0,<1.29.0)"] +iot-roborunner = ["mypy-boto3-iot-roborunner (>=1.28.0,<1.29.0)"] +iot1click-devices = ["mypy-boto3-iot1click-devices (>=1.28.0,<1.29.0)"] +iot1click-projects = ["mypy-boto3-iot1click-projects (>=1.28.0,<1.29.0)"] +iotanalytics = ["mypy-boto3-iotanalytics (>=1.28.0,<1.29.0)"] +iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.28.0,<1.29.0)"] +iotevents = ["mypy-boto3-iotevents (>=1.28.0,<1.29.0)"] +iotevents-data = ["mypy-boto3-iotevents-data (>=1.28.0,<1.29.0)"] +iotfleethub = ["mypy-boto3-iotfleethub (>=1.28.0,<1.29.0)"] +iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.28.0,<1.29.0)"] +iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.28.0,<1.29.0)"] +iotsitewise = ["mypy-boto3-iotsitewise (>=1.28.0,<1.29.0)"] +iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.28.0,<1.29.0)"] +iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.28.0,<1.29.0)"] +iotwireless = ["mypy-boto3-iotwireless (>=1.28.0,<1.29.0)"] +ivs = ["mypy-boto3-ivs (>=1.28.0,<1.29.0)"] +ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.28.0,<1.29.0)"] +ivschat = ["mypy-boto3-ivschat (>=1.28.0,<1.29.0)"] +kafka = ["mypy-boto3-kafka (>=1.28.0,<1.29.0)"] +kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.28.0,<1.29.0)"] +kendra = ["mypy-boto3-kendra (>=1.28.0,<1.29.0)"] +kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.28.0,<1.29.0)"] +keyspaces = ["mypy-boto3-keyspaces (>=1.28.0,<1.29.0)"] +kinesis = ["mypy-boto3-kinesis (>=1.28.0,<1.29.0)"] +kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.28.0,<1.29.0)"] +kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.28.0,<1.29.0)"] +kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.28.0,<1.29.0)"] +kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.28.0,<1.29.0)"] +kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.28.0,<1.29.0)"] +kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.28.0,<1.29.0)"] +kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.28.0,<1.29.0)"] +kms = ["mypy-boto3-kms (>=1.28.0,<1.29.0)"] +lakeformation = ["mypy-boto3-lakeformation (>=1.28.0,<1.29.0)"] +lambda = ["mypy-boto3-lambda (>=1.28.0,<1.29.0)"] +lex-models = ["mypy-boto3-lex-models (>=1.28.0,<1.29.0)"] +lex-runtime = ["mypy-boto3-lex-runtime (>=1.28.0,<1.29.0)"] +lexv2-models = ["mypy-boto3-lexv2-models (>=1.28.0,<1.29.0)"] +lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.28.0,<1.29.0)"] +license-manager = ["mypy-boto3-license-manager (>=1.28.0,<1.29.0)"] +license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.28.0,<1.29.0)"] +license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.28.0,<1.29.0)"] +lightsail = ["mypy-boto3-lightsail (>=1.28.0,<1.29.0)"] +location = ["mypy-boto3-location (>=1.28.0,<1.29.0)"] +logs = ["mypy-boto3-logs (>=1.28.0,<1.29.0)"] +lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.28.0,<1.29.0)"] +lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.28.0,<1.29.0)"] +lookoutvision = ["mypy-boto3-lookoutvision (>=1.28.0,<1.29.0)"] +m2 = ["mypy-boto3-m2 (>=1.28.0,<1.29.0)"] +machinelearning = ["mypy-boto3-machinelearning (>=1.28.0,<1.29.0)"] +macie = ["mypy-boto3-macie (>=1.28.0,<1.29.0)"] +macie2 = ["mypy-boto3-macie2 (>=1.28.0,<1.29.0)"] +managedblockchain = ["mypy-boto3-managedblockchain (>=1.28.0,<1.29.0)"] +managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.28.0,<1.29.0)"] +marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.28.0,<1.29.0)"] +marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.28.0,<1.29.0)"] +marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.28.0,<1.29.0)"] +mediaconnect = ["mypy-boto3-mediaconnect (>=1.28.0,<1.29.0)"] +mediaconvert = ["mypy-boto3-mediaconvert (>=1.28.0,<1.29.0)"] +medialive = ["mypy-boto3-medialive (>=1.28.0,<1.29.0)"] +mediapackage = ["mypy-boto3-mediapackage (>=1.28.0,<1.29.0)"] +mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.28.0,<1.29.0)"] +mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.28.0,<1.29.0)"] +mediastore = ["mypy-boto3-mediastore (>=1.28.0,<1.29.0)"] +mediastore-data = ["mypy-boto3-mediastore-data (>=1.28.0,<1.29.0)"] +mediatailor = ["mypy-boto3-mediatailor (>=1.28.0,<1.29.0)"] +medical-imaging = ["mypy-boto3-medical-imaging (>=1.28.0,<1.29.0)"] +memorydb = ["mypy-boto3-memorydb (>=1.28.0,<1.29.0)"] +meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.28.0,<1.29.0)"] +mgh = ["mypy-boto3-mgh (>=1.28.0,<1.29.0)"] +mgn = ["mypy-boto3-mgn (>=1.28.0,<1.29.0)"] +migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.28.0,<1.29.0)"] +migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.28.0,<1.29.0)"] +migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.28.0,<1.29.0)"] +migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.28.0,<1.29.0)"] +mobile = ["mypy-boto3-mobile (>=1.28.0,<1.29.0)"] +mq = ["mypy-boto3-mq (>=1.28.0,<1.29.0)"] +mturk = ["mypy-boto3-mturk (>=1.28.0,<1.29.0)"] +mwaa = ["mypy-boto3-mwaa (>=1.28.0,<1.29.0)"] +neptune = ["mypy-boto3-neptune (>=1.28.0,<1.29.0)"] +network-firewall = ["mypy-boto3-network-firewall (>=1.28.0,<1.29.0)"] +networkmanager = ["mypy-boto3-networkmanager (>=1.28.0,<1.29.0)"] +nimble = ["mypy-boto3-nimble (>=1.28.0,<1.29.0)"] +oam = ["mypy-boto3-oam (>=1.28.0,<1.29.0)"] +omics = ["mypy-boto3-omics (>=1.28.0,<1.29.0)"] +opensearch = ["mypy-boto3-opensearch (>=1.28.0,<1.29.0)"] +opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.28.0,<1.29.0)"] +opsworks = ["mypy-boto3-opsworks (>=1.28.0,<1.29.0)"] +opsworkscm = ["mypy-boto3-opsworkscm (>=1.28.0,<1.29.0)"] +organizations = ["mypy-boto3-organizations (>=1.28.0,<1.29.0)"] +osis = ["mypy-boto3-osis (>=1.28.0,<1.29.0)"] +outposts = ["mypy-boto3-outposts (>=1.28.0,<1.29.0)"] +panorama = ["mypy-boto3-panorama (>=1.28.0,<1.29.0)"] +payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.28.0,<1.29.0)"] +payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.28.0,<1.29.0)"] +personalize = ["mypy-boto3-personalize (>=1.28.0,<1.29.0)"] +personalize-events = ["mypy-boto3-personalize-events (>=1.28.0,<1.29.0)"] +personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.28.0,<1.29.0)"] +pi = ["mypy-boto3-pi (>=1.28.0,<1.29.0)"] +pinpoint = ["mypy-boto3-pinpoint (>=1.28.0,<1.29.0)"] +pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.28.0,<1.29.0)"] +pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.28.0,<1.29.0)"] +pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.28.0,<1.29.0)"] +pipes = ["mypy-boto3-pipes (>=1.28.0,<1.29.0)"] +polly = ["mypy-boto3-polly (>=1.28.0,<1.29.0)"] +pricing = ["mypy-boto3-pricing (>=1.28.0,<1.29.0)"] +privatenetworks = ["mypy-boto3-privatenetworks (>=1.28.0,<1.29.0)"] +proton = ["mypy-boto3-proton (>=1.28.0,<1.29.0)"] +qldb = ["mypy-boto3-qldb (>=1.28.0,<1.29.0)"] +qldb-session = ["mypy-boto3-qldb-session (>=1.28.0,<1.29.0)"] +quicksight = ["mypy-boto3-quicksight (>=1.28.0,<1.29.0)"] +ram = ["mypy-boto3-ram (>=1.28.0,<1.29.0)"] +rbin = ["mypy-boto3-rbin (>=1.28.0,<1.29.0)"] +rds = ["mypy-boto3-rds (>=1.28.0,<1.29.0)"] +rds-data = ["mypy-boto3-rds-data (>=1.28.0,<1.29.0)"] +redshift = ["mypy-boto3-redshift (>=1.28.0,<1.29.0)"] +redshift-data = ["mypy-boto3-redshift-data (>=1.28.0,<1.29.0)"] +redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.28.0,<1.29.0)"] +rekognition = ["mypy-boto3-rekognition (>=1.28.0,<1.29.0)"] +resiliencehub = ["mypy-boto3-resiliencehub (>=1.28.0,<1.29.0)"] +resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.28.0,<1.29.0)"] +resource-groups = ["mypy-boto3-resource-groups (>=1.28.0,<1.29.0)"] +resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.28.0,<1.29.0)"] +robomaker = ["mypy-boto3-robomaker (>=1.28.0,<1.29.0)"] +rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.28.0,<1.29.0)"] +route53 = ["mypy-boto3-route53 (>=1.28.0,<1.29.0)"] +route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.28.0,<1.29.0)"] +route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.28.0,<1.29.0)"] +route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.28.0,<1.29.0)"] +route53domains = ["mypy-boto3-route53domains (>=1.28.0,<1.29.0)"] +route53resolver = ["mypy-boto3-route53resolver (>=1.28.0,<1.29.0)"] +rum = ["mypy-boto3-rum (>=1.28.0,<1.29.0)"] +s3 = ["mypy-boto3-s3 (>=1.28.0,<1.29.0)"] +s3control = ["mypy-boto3-s3control (>=1.28.0,<1.29.0)"] +s3outposts = ["mypy-boto3-s3outposts (>=1.28.0,<1.29.0)"] +sagemaker = ["mypy-boto3-sagemaker (>=1.28.0,<1.29.0)"] +sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.28.0,<1.29.0)"] +sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.28.0,<1.29.0)"] +sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.28.0,<1.29.0)"] +sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.28.0,<1.29.0)"] +sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.28.0,<1.29.0)"] +sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.28.0,<1.29.0)"] +savingsplans = ["mypy-boto3-savingsplans (>=1.28.0,<1.29.0)"] +scheduler = ["mypy-boto3-scheduler (>=1.28.0,<1.29.0)"] +schemas = ["mypy-boto3-schemas (>=1.28.0,<1.29.0)"] +sdb = ["mypy-boto3-sdb (>=1.28.0,<1.29.0)"] +secretsmanager = ["mypy-boto3-secretsmanager (>=1.28.0,<1.29.0)"] +securityhub = ["mypy-boto3-securityhub (>=1.28.0,<1.29.0)"] +securitylake = ["mypy-boto3-securitylake (>=1.28.0,<1.29.0)"] +serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.28.0,<1.29.0)"] +service-quotas = ["mypy-boto3-service-quotas (>=1.28.0,<1.29.0)"] +servicecatalog = ["mypy-boto3-servicecatalog (>=1.28.0,<1.29.0)"] +servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.28.0,<1.29.0)"] +servicediscovery = ["mypy-boto3-servicediscovery (>=1.28.0,<1.29.0)"] +ses = ["mypy-boto3-ses (>=1.28.0,<1.29.0)"] +sesv2 = ["mypy-boto3-sesv2 (>=1.28.0,<1.29.0)"] +shield = ["mypy-boto3-shield (>=1.28.0,<1.29.0)"] +signer = ["mypy-boto3-signer (>=1.28.0,<1.29.0)"] +simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.28.0,<1.29.0)"] +sms = ["mypy-boto3-sms (>=1.28.0,<1.29.0)"] +sms-voice = ["mypy-boto3-sms-voice (>=1.28.0,<1.29.0)"] +snow-device-management = ["mypy-boto3-snow-device-management (>=1.28.0,<1.29.0)"] +snowball = ["mypy-boto3-snowball (>=1.28.0,<1.29.0)"] +sns = ["mypy-boto3-sns (>=1.28.0,<1.29.0)"] +sqs = ["mypy-boto3-sqs (>=1.28.0,<1.29.0)"] +ssm = ["mypy-boto3-ssm (>=1.28.0,<1.29.0)"] +ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.28.0,<1.29.0)"] +ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.28.0,<1.29.0)"] +ssm-sap = ["mypy-boto3-ssm-sap (>=1.28.0,<1.29.0)"] +sso = ["mypy-boto3-sso (>=1.28.0,<1.29.0)"] +sso-admin = ["mypy-boto3-sso-admin (>=1.28.0,<1.29.0)"] +sso-oidc = ["mypy-boto3-sso-oidc (>=1.28.0,<1.29.0)"] +stepfunctions = ["mypy-boto3-stepfunctions (>=1.28.0,<1.29.0)"] +storagegateway = ["mypy-boto3-storagegateway (>=1.28.0,<1.29.0)"] +sts = ["mypy-boto3-sts (>=1.28.0,<1.29.0)"] +support = ["mypy-boto3-support (>=1.28.0,<1.29.0)"] +support-app = ["mypy-boto3-support-app (>=1.28.0,<1.29.0)"] +swf = ["mypy-boto3-swf (>=1.28.0,<1.29.0)"] +synthetics = ["mypy-boto3-synthetics (>=1.28.0,<1.29.0)"] +textract = ["mypy-boto3-textract (>=1.28.0,<1.29.0)"] +timestream-query = ["mypy-boto3-timestream-query (>=1.28.0,<1.29.0)"] +timestream-write = ["mypy-boto3-timestream-write (>=1.28.0,<1.29.0)"] +tnb = ["mypy-boto3-tnb (>=1.28.0,<1.29.0)"] +transcribe = ["mypy-boto3-transcribe (>=1.28.0,<1.29.0)"] +transfer = ["mypy-boto3-transfer (>=1.28.0,<1.29.0)"] +translate = ["mypy-boto3-translate (>=1.28.0,<1.29.0)"] +verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.28.0,<1.29.0)"] +voice-id = ["mypy-boto3-voice-id (>=1.28.0,<1.29.0)"] +vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.28.0,<1.29.0)"] +waf = ["mypy-boto3-waf (>=1.28.0,<1.29.0)"] +waf-regional = ["mypy-boto3-waf-regional (>=1.28.0,<1.29.0)"] +wafv2 = ["mypy-boto3-wafv2 (>=1.28.0,<1.29.0)"] +wellarchitected = ["mypy-boto3-wellarchitected (>=1.28.0,<1.29.0)"] +wisdom = ["mypy-boto3-wisdom (>=1.28.0,<1.29.0)"] +workdocs = ["mypy-boto3-workdocs (>=1.28.0,<1.29.0)"] +worklink = ["mypy-boto3-worklink (>=1.28.0,<1.29.0)"] +workmail = ["mypy-boto3-workmail (>=1.28.0,<1.29.0)"] +workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.28.0,<1.29.0)"] +workspaces = ["mypy-boto3-workspaces (>=1.28.0,<1.29.0)"] +workspaces-web = ["mypy-boto3-workspaces-web (>=1.28.0,<1.29.0)"] +xray = ["mypy-boto3-xray (>=1.28.0,<1.29.0)"] + +[[package]] +name = "botocore" +version = "1.31.20" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">= 3.7" +files = [ + {file = "botocore-1.31.20-py3-none-any.whl", hash = "sha256:be51c5352162700e7beb0aa27af394adbbf86f8e7a2ca0c437d448d0a7b2bdfb"}, + {file = "botocore-1.31.20.tar.gz", hash = "sha256:485ef175cd011ebc965f4577d8cc02a226c46bd608dd2bb75ce6938328cff0fd"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.16.26)"] + +[[package]] +name = "botocore-stubs" +version = "1.31.20" +description = "Type annotations and code completion for botocore" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "botocore_stubs-1.31.20-py3-none-any.whl", hash = "sha256:4608756e770398104e5577b9dbc90654c2bebf39d72aaf28536933dabe5eaf0b"}, + {file = "botocore_stubs-1.31.20.tar.gz", hash = "sha256:4aa97c672a9ce162363bf177d003bd715c06a86f9fb69353515cc1ea00b4f163"}, +] + +[package.dependencies] +types-awscrt = "*" +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "bowler" +version = "0.9.0" +description = "Safe code refactoring for modern Python projects" +optional = false +python-versions = ">=3.6" +files = [ + {file = "bowler-0.9.0-py3-none-any.whl", hash = "sha256:0351839e9917765be694aa52c99ea784dc1286c9bdd6fd066b810097fc273e1b"}, + {file = "bowler-0.9.0.tar.gz", hash = "sha256:cdb85ce2e7bd545802a15d755d1daf2b6a125429355c50d2019a9f35d63e45db"}, +] + +[package.dependencies] +attrs = "*" +click = "*" +fissix = "*" +moreorless = ">=0.2.0" +volatile = "*" + +[[package]] +name = "build" +version = "0.10.0" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "build-0.10.0-py3-none-any.whl", hash = "sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171"}, + {file = "build-0.10.0.tar.gz", hash = "sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +packaging = ">=19.0" +pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2021.08.31)", "sphinx (>=4.0,<5.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"] +test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "toml (>=0.10.0)", "wheel (>=0.36.0)"] +typing = ["importlib-metadata (>=5.1)", "mypy (==0.991)", "tomli", "typing-extensions (>=3.7.4.3)"] +virtualenv = ["virtualenv (>=20.0.35)"] + +[[package]] +name = "bytecode" +version = "0.14.2" +description = "Python module to generate and modify bytecode" +optional = false +python-versions = ">=3.8" +files = [ + {file = "bytecode-0.14.2-py3-none-any.whl", hash = "sha256:e368a2b9bbd7c986133c951250db94fb32f774cfc49752a9db9073bcf9899762"}, + {file = "bytecode-0.14.2.tar.gz", hash = "sha256:386378d9025d68ddb144870ae74330a492717b11b8c9164c4034e88add808f0c"}, +] + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[[package]] +name = "cattrs" +version = "23.1.2" +description = "Composable complex class support for attrs and dataclasses." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cattrs-23.1.2-py3-none-any.whl", hash = "sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4"}, + {file = "cattrs-23.1.2.tar.gz", hash = "sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657"}, +] + +[package.dependencies] +attrs = ">=20" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +bson = ["pymongo (>=4.2.0,<5.0.0)"] +cbor2 = ["cbor2 (>=5.4.6,<6.0.0)"] +msgpack = ["msgpack (>=1.0.2,<2.0.0)"] +orjson = ["orjson (>=3.5.2,<4.0.0)"] +pyyaml = ["PyYAML (>=6.0,<7.0)"] +tomlkit = ["tomlkit (>=0.11.4,<0.12.0)"] +ujson = ["ujson (>=5.4.0,<6.0.0)"] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.6" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "cloudpickle" +version = "2.2.1" +description = "Extended pickling support for Python objects" +optional = false +python-versions = ">=3.6" +files = [ + {file = "cloudpickle-2.2.1-py3-none-any.whl", hash = "sha256:61f594d1f4c295fa5cd9014ceb3a1fc4a70b0de1164b94fbc2d854ccba056f9f"}, + {file = "cloudpickle-2.2.1.tar.gz", hash = "sha256:d89684b8de9e34a2a43b3460fbca07d09d6e25ce858df4d5a44240403b6178f5"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "comm" +version = "0.1.4" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.6" +files = [ + {file = "comm-0.1.4-py3-none-any.whl", hash = "sha256:6d52794cba11b36ed9860999cd10fd02d6b2eac177068fdd585e1e2f8a96e67a"}, + {file = "comm-0.1.4.tar.gz", hash = "sha256:354e40a59c9dd6db50c5cc6b4acc887d82e9603787f83b68c01a80a923984d15"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +lint = ["black (>=22.6.0)", "mdformat (>0.7)", "mdformat-gfm (>=0.3.5)", "ruff (>=0.0.156)"] +test = ["pytest"] +typing = ["mypy (>=0.990)"] + +[[package]] +name = "cryptography" +version = "40.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.6" +files = [ + {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"}, + {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"}, + {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"}, + {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"}, + {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"}, + {file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"}, + {file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"}, + {file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"}, + {file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"}, + {file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "check-manifest", "mypy", "ruff"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] + +[[package]] +name = "dask" +version = "2023.5.0" +description = "Parallel PyData with Task Scheduling" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dask-2023.5.0-py3-none-any.whl", hash = "sha256:32b34986519b7ddc0947c8ca63c2fc81b964e4c208dfb5cbf9f4f8aec92d152b"}, + {file = "dask-2023.5.0.tar.gz", hash = "sha256:4f4c28ac406e81b8f21b5be4b31b21308808f3e0e7c7e2f4a914f16476d9941b"}, +] + +[package.dependencies] +click = ">=8.0" +cloudpickle = ">=1.5.0" +fsspec = ">=2021.09.0" +importlib-metadata = ">=4.13.0" +packaging = ">=20.0" +partd = ">=1.2.0" +pyyaml = ">=5.3.1" +toolz = ">=0.10.0" + +[package.extras] +array = ["numpy (>=1.21)"] +complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)"] +dataframe = ["numpy (>=1.21)", "pandas (>=1.3)"] +diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] +distributed = ["distributed (==2023.5.0)"] +test = ["pandas[test]", "pre-commit", "pytest", "pytest-rerunfailures", "pytest-xdist"] + +[[package]] +name = "ddsketch" +version = "2.0.4" +description = "Distributed quantile sketches" +optional = false +python-versions = ">=2.7" +files = [ + {file = "ddsketch-2.0.4-py3-none-any.whl", hash = "sha256:3227a270fd686a29d3a7128f9352ccf852314410380fc11384356f1ae2a75938"}, + {file = "ddsketch-2.0.4.tar.gz", hash = "sha256:32f7314077fec8747d4faebaec2c854b5ffc399c5f552f73fa94024f48d74d64"}, +] + +[package.dependencies] +protobuf = {version = ">=3.0.0", markers = "python_version >= \"3.7\""} +six = "*" + +[[package]] +name = "ddtrace" +version = "1.17.3" +description = "Datadog APM client library" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "ddtrace-1.17.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:f64a647cd45fae72d3db653c186658162cc2f89a23db130b318c718f4b4a125e"}, + {file = "ddtrace-1.17.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:50b421059a41b477e8a046474fb8c886798be40c88bdb961d1feb3c00abc9482"}, + {file = "ddtrace-1.17.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e186cfcc9b8da04ee8151002325c4e6a8e006e30ee14b60d80718e20d2c4e05d"}, + {file = "ddtrace-1.17.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c8a69000457197b651eccdac971b9c8f48d8cd110af591c3020c83d2727015c9"}, + {file = "ddtrace-1.17.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:08c274a999bd1e72b2fb939065d8ffa7d26ab888feee2bdb49a26055e52ce378"}, + {file = "ddtrace-1.17.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:aab551b75fcd22404f464c29f4f053ebc1201b2cc76349c3585c93fb07e90d45"}, + {file = "ddtrace-1.17.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:795a128f1b0a347ff1da5455e5d35b7472abace522ecd316746ebd4c0a4a5779"}, + {file = "ddtrace-1.17.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a876e391835bd2f87de12cce21e2fd907feb70f34b56eb28d420b875430b730d"}, + {file = "ddtrace-1.17.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3657eb6732803af60af038ee49068ef662badf199b1d1807a88993284f5930"}, + {file = "ddtrace-1.17.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:254fdd83087a8acabbf6575efaeef406a58ed98271a864a9871bbdadcaeacf23"}, + {file = "ddtrace-1.17.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:afa11fb38004b87e25b2bdc8da78e811c7bf79744e05267e8be925aa97e37888"}, + {file = "ddtrace-1.17.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3d89df96c2b4f5ffc8dc7631a115e0af9a44d421d4b6e57f9d19579ba19d168f"}, + {file = "ddtrace-1.17.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:03b43facdd40e16f6e393125c7ccf835621fb4acb543c559645bd0a4b07a379f"}, + {file = "ddtrace-1.17.3-cp310-cp310-win32.whl", hash = "sha256:bff1bbd56870ed76510f72ef04cbbe57c199dbb6171c4d53159e65d4b045b081"}, + {file = "ddtrace-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:04f2f0c776a29917a08ba1bb05d5880df8090b6d5833b0a9bb7b7ed5b1395c24"}, + {file = "ddtrace-1.17.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c99f67faa05fc91f099adb2ce7c5bafb850191b067d28b08e520ebef1681112b"}, + {file = "ddtrace-1.17.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:43dbf760edd883b6fe0fa415eb45fd0def90eaeb2b66805d8eaa08f0682b643e"}, + {file = "ddtrace-1.17.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d778a00abbf5b80cd422ecc4b9bddc95323fcb44cd33c775e1b29ba6f9df8167"}, + {file = "ddtrace-1.17.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:011bb386d7960755fd83d81b41a6c053015ed982cebeb659a7da580a82cbe228"}, + {file = "ddtrace-1.17.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6055e0fa87e38f88a162f5ac629a1f1b31a3dc03558aaf3cb4649bb393115b84"}, + {file = "ddtrace-1.17.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d14cde9b8490366831276eadcddba94f892c592382a091149172db0caacb3f65"}, + {file = "ddtrace-1.17.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4a4471a20247c77015b640f11dc0f4536f0e1c8ca12969fabf04cb4af705fe8d"}, + {file = "ddtrace-1.17.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d622cf71b495d38d097a287a0c28d51a4d0afe1f6a7e02080ed3bddfc4c1ee4"}, + {file = "ddtrace-1.17.3-cp311-cp311-win32.whl", hash = "sha256:a514aedd59c8a278ccf2074cff2c83119bde2fdede5b4e46b64bff620e3e134c"}, + {file = "ddtrace-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:cad36ca0c7ddb5075bf4a86305c1ceaacdcace939c469a4f1578e9783c373e42"}, + {file = "ddtrace-1.17.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:321c95f9f9798e57ef937e1838a677c346e2cc8202fabeac26efbb5af06029dc"}, + {file = "ddtrace-1.17.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0e23fb40487d15172667ece53a89f23be1d4c2fe2578a3bd1f59d87c222ac434"}, + {file = "ddtrace-1.17.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:30378c9723c8501148731d311d23bb9fa735ec55b65ec86c7d1dbbdf8ad87326"}, + {file = "ddtrace-1.17.3-cp35-cp35m-win32.whl", hash = "sha256:790322e29326d9abca61b138b6f30a39d2935f26854e0f4290995f8c249f67a5"}, + {file = "ddtrace-1.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:3a674580f5911e9916d7ad79a94854979404ed95da030aa964c00865cf5cd550"}, + {file = "ddtrace-1.17.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:bc2629756d1e5b33b519456bf32b6ce527fd733fa1dc36af6bea4c44b62f100d"}, + {file = "ddtrace-1.17.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8cc940c45756aaf7c6222faf564f972e5921933c9be714f21495b18aa5b467b"}, + {file = "ddtrace-1.17.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c06b1fe25ed8ae9f947fc30850914426a15daaeaa3d5a7c1cd961682858c8e52"}, + {file = "ddtrace-1.17.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4fc2597410a84d640f7a034049d32872a93c1b47ccfc9eaedb96970842c9c43"}, + {file = "ddtrace-1.17.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:674da4f461fadeda25719c11f3e6e2f399d5db6a1fbe519138ed8c25ac0a4dcd"}, + {file = "ddtrace-1.17.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:992cf6dd2af32d637c1c9f4897c91192a9a9644b3bcba7bbd1e990c0b177f734"}, + {file = "ddtrace-1.17.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:98059335b5c5e966098aaabb8c8d1f7e8d6c361f0ed2b00ed9b4957bb880a086"}, + {file = "ddtrace-1.17.3-cp36-cp36m-win32.whl", hash = "sha256:ad1bd2a5b7271b8c1023cb4889a8226e1cf47e2c96ec66c2cec187ae38428312"}, + {file = "ddtrace-1.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:417d056b1e2fd204abd094d813cd95f5354a432e32fa87984b4027d0d319aee2"}, + {file = "ddtrace-1.17.3-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:a04aa4aa8c527eb7cbda005f419a91f21236fe1ca0779a542061e145729ba957"}, + {file = "ddtrace-1.17.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e5b9f583cc564e13426eb8fed5e3b410400d16874e8adb68fecc9e3c1fd2a0b"}, + {file = "ddtrace-1.17.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1be4d94c903bb92fe9e3ac85ba845d3636198efafce4ffcbcf7c44ec3e6b0e2f"}, + {file = "ddtrace-1.17.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1769ebce6d61ac239b6384f528c378eab9f33e6cbb9b40645ed14c90ee2cdb"}, + {file = "ddtrace-1.17.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a1534dff188e20fad4c90cc3bc4be025a9f702ac1b1e495abbb6997a7df23d3"}, + {file = "ddtrace-1.17.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b56571a3855992dd66c6c0d65c1a6c6acf3f1d7d39803bc20cdd0c0fa27f6987"}, + {file = "ddtrace-1.17.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d9d22b36cbbc23065fde04973c9a5119e1a60e96e5a1a1b731f186684bca0cc3"}, + {file = "ddtrace-1.17.3-cp37-cp37m-win32.whl", hash = "sha256:b439516904f6a41b5d72b8c62b7b0131f52c32c15c724cd5b35e656cae3e78df"}, + {file = "ddtrace-1.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:74e33cd43019ee31e24b9276795a2aea4a56671f52fc01ee4d39eeff44212f86"}, + {file = "ddtrace-1.17.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7d5059717f95243279cb34980649a3806237ac6145ca1b7b9147feeff472ded3"}, + {file = "ddtrace-1.17.3-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:64411cff66db07939fd42068430fd49fb10e592eff99ba0b1cceba3da0b693e5"}, + {file = "ddtrace-1.17.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eff94790b5f1567ae07835d3198fa5f7c0a0738ac3fb2a4e1b0447c3d5ee8e38"}, + {file = "ddtrace-1.17.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:357e3db56e8cbf629b9861005b77790417fef8525276ce566443bbf817f59238"}, + {file = "ddtrace-1.17.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4b90466bff4c0c3f2217a33e3058e5fcc11a645488606dfa7c138db72b1fdd6"}, + {file = "ddtrace-1.17.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8063d6d7b64159b197726b8f344629be611e26928e25a5d6256a160b03d6db51"}, + {file = "ddtrace-1.17.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:aa6a82af93e5dfbd60bd343a8c8f71524281c1fbbcc6dd14ad14fbc6d712c09c"}, + {file = "ddtrace-1.17.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6560edbf9250613fc1955b3dde3a7223ca54c7e70a0909838e211d61d86c19b2"}, + {file = "ddtrace-1.17.3-cp38-cp38-win32.whl", hash = "sha256:5e6c9ea56771e9e3116670e5a4bfb6fa7e13c0ef2eb8f9600652c4d15fd2a027"}, + {file = "ddtrace-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:454d4ce8d3868b128a4839b678a101241458129867ca968185bd9a52c9bd3f00"}, + {file = "ddtrace-1.17.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:f568773376a12df1fe754c02c6ad69ed2bed36e27d333ecedabdf417867302c6"}, + {file = "ddtrace-1.17.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:6b7bda6fc2077bfb2e51be238cf994ea29be99772c5afe6ae2b87bceee723689"}, + {file = "ddtrace-1.17.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46e5ef1d5507c9cb98bbb7d6b4e58e1a9e0bbaf659c76b1894d16706ac7179b7"}, + {file = "ddtrace-1.17.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:107dda40004c32a2462e1971dacf0262461e526093db3e0636791ae79bb23608"}, + {file = "ddtrace-1.17.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e53d715ade7e57e2874a834cd2fb724638e714ead7cf1f1a2ea65a66b3638c2"}, + {file = "ddtrace-1.17.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e3b658e02c67e503744a6c8091be7dbd6c603c33228ed374f230b9f0aa56c92"}, + {file = "ddtrace-1.17.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ce85a879126ccac03b1547d99a1e7a2db7169e66be4d7161b8669c8d2b5931f"}, + {file = "ddtrace-1.17.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0d1194a99b6b1058d427d1d035ee817a4b080a1586463f425788526b197bb3d9"}, + {file = "ddtrace-1.17.3-cp39-cp39-win32.whl", hash = "sha256:0119ea42e6d659975a5fc35d4da4209ca3cf886e835175b8b6dc56b2428e1f05"}, + {file = "ddtrace-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:9f3dcc162a371bf159a4963754c0aad0c4439b4ec6d8d86657567a933cb4809c"}, + {file = "ddtrace-1.17.3.tar.gz", hash = "sha256:f21e8870eef6a7d7d428b54e4dba18b302272d60ce6b49cae02edb71bd1a782f"}, +] + +[package.dependencies] +attrs = {version = ">=20", markers = "python_version > \"2.7\""} +bytecode = {version = "*", markers = "python_version >= \"3.8\""} +cattrs = {version = "*", markers = "python_version >= \"3.7\""} +ddsketch = ">=2.0.1" +envier = "*" +opentelemetry-api = {version = ">=1", markers = "python_version >= \"3.7\""} +protobuf = {version = ">=3", markers = "python_version >= \"3.7\""} +six = ">=1.12.0" +typing-extensions = "*" +xmltodict = ">=0.12" + +[package.extras] +opentracing = ["opentracing (>=2.0.0)"] + +[[package]] +name = "debugpy" +version = "1.6.7" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "debugpy-1.6.7-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b3e7ac809b991006ad7f857f016fa92014445085711ef111fdc3f74f66144096"}, + {file = "debugpy-1.6.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3876611d114a18aafef6383695dfc3f1217c98a9168c1aaf1a02b01ec7d8d1e"}, + {file = "debugpy-1.6.7-cp310-cp310-win32.whl", hash = "sha256:33edb4afa85c098c24cc361d72ba7c21bb92f501104514d4ffec1fb36e09c01a"}, + {file = "debugpy-1.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:ed6d5413474e209ba50b1a75b2d9eecf64d41e6e4501977991cdc755dc83ab0f"}, + {file = "debugpy-1.6.7-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:38ed626353e7c63f4b11efad659be04c23de2b0d15efff77b60e4740ea685d07"}, + {file = "debugpy-1.6.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279d64c408c60431c8ee832dfd9ace7c396984fd7341fa3116aee414e7dcd88d"}, + {file = "debugpy-1.6.7-cp37-cp37m-win32.whl", hash = "sha256:dbe04e7568aa69361a5b4c47b4493d5680bfa3a911d1e105fbea1b1f23f3eb45"}, + {file = "debugpy-1.6.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f90a2d4ad9a035cee7331c06a4cf2245e38bd7c89554fe3b616d90ab8aab89cc"}, + {file = "debugpy-1.6.7-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:5224eabbbeddcf1943d4e2821876f3e5d7d383f27390b82da5d9558fd4eb30a9"}, + {file = "debugpy-1.6.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae1123dff5bfe548ba1683eb972329ba6d646c3a80e6b4c06cd1b1dd0205e9b"}, + {file = "debugpy-1.6.7-cp38-cp38-win32.whl", hash = "sha256:9cd10cf338e0907fdcf9eac9087faa30f150ef5445af5a545d307055141dd7a4"}, + {file = "debugpy-1.6.7-cp38-cp38-win_amd64.whl", hash = "sha256:aaf6da50377ff4056c8ed470da24632b42e4087bc826845daad7af211e00faad"}, + {file = "debugpy-1.6.7-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:0679b7e1e3523bd7d7869447ec67b59728675aadfc038550a63a362b63029d2c"}, + {file = "debugpy-1.6.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de86029696e1b3b4d0d49076b9eba606c226e33ae312a57a46dca14ff370894d"}, + {file = "debugpy-1.6.7-cp39-cp39-win32.whl", hash = "sha256:d71b31117779d9a90b745720c0eab54ae1da76d5b38c8026c654f4a066b0130a"}, + {file = "debugpy-1.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:c0ff93ae90a03b06d85b2c529eca51ab15457868a377c4cc40a23ab0e4e552a3"}, + {file = "debugpy-1.6.7-py2.py3-none-any.whl", hash = "sha256:53f7a456bc50706a0eaabecf2d3ce44c4d5010e46dfc65b6b81a518b42866267"}, + {file = "debugpy-1.6.7.zip", hash = "sha256:c4c2f0810fa25323abfdfa36cbbbb24e5c3b1a42cb762782de64439c575d67f2"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + +[[package]] +name = "dill" +version = "0.3.7" +description = "serialize all of Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "docutils" +version = "0.20.1" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, +] + +[[package]] +name = "envier" +version = "0.4.0" +description = "Python application configuration via the environment" +optional = false +python-versions = ">=2.7" +files = [ + {file = "envier-0.4.0-py3-none-any.whl", hash = "sha256:7b91af0f16ea3e56d91ec082f038987e81b441fc19c657a8b8afe0909740a706"}, + {file = "envier-0.4.0.tar.gz", hash = "sha256:e68dcd1ed67d8b6313883e27dff3e701b7fba944d2ed4b7f53d0cc2e12364a82"}, +] + +[package.extras] +mypy = ["mypy"] + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = "*" +files = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, +] + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + +[[package]] +name = "fastapi" +version = "0.95.2" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.7" +files = [ + {file = "fastapi-0.95.2-py3-none-any.whl", hash = "sha256:d374dbc4ef2ad9b803899bd3360d34c534adc574546e25314ab72c0c4411749f"}, + {file = "fastapi-0.95.2.tar.gz", hash = "sha256:4d9d3e8c71c73f11874bcf5e33626258d143252e329a01002f767306c64fb982"}, +] + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = ">=0.27.0,<0.28.0" + +[package.extras] +all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] + +[[package]] +name = "fastavro" +version = "1.8.2" +description = "Fast read/write of AVRO files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastavro-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:0e08964b2e9a455d831f2557402a683d4c4d45206f2ab9ade7c69d3dc14e0e58"}, + {file = "fastavro-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:401a70b1e5c7161420c6019e0c8afa88f7c8a373468591f5ec37639a903c2509"}, + {file = "fastavro-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef1ed3eaa4240c05698d02d8d0c010b9a03780eda37b492da6cd4c9d37e04ec"}, + {file = "fastavro-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:543185a672ff6306beb329b57a7b8a3a2dd1eb21a5ccc530150623d58d48bb98"}, + {file = "fastavro-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ffbf8bae1edb50fe7beeffc3afa8e684686550c2e5d31bf01c25cfa213f581e1"}, + {file = "fastavro-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:bb545eb9d876bc7b785e27e98e7720ada7eee7d7a1729798d2ed51517f13500a"}, + {file = "fastavro-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b837d3038c651046252bc92c1b9899bf21c7927a148a1ff89599c36c2a331ca"}, + {file = "fastavro-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3510e96c0a47e4e914bd1a29c954eb662bfa24849ad92e597cb97cc79f21af7"}, + {file = "fastavro-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccc0e74f2c2ab357f39bb73d67fcdb6dc10e23fdbbd399326139f72ec0fb99a3"}, + {file = "fastavro-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:add51c70d0ab1175601c75cd687bbe9d16ae312cd8899b907aafe0d79ee2bc1d"}, + {file = "fastavro-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d9e2662f57e6453e9a2c9fb4f54b2a9e62e3e46f5a412ac00558112336d23883"}, + {file = "fastavro-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:fea75cf53a93c56dd56e68abce8d314ef877b27451c870cd7ede7582d34c08a7"}, + {file = "fastavro-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:f489020bb8664c2737c03457ad5dbd490579ddab6f0a7b5c17fecfe982715a89"}, + {file = "fastavro-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a547625c138efd5e61300119241041906ee8cb426fc7aa789900f87af7ed330d"}, + {file = "fastavro-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53beb458f30c9ad4aa7bff4a42243ff990ffb713b6ce0cd9b360cbc3d648fe52"}, + {file = "fastavro-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7b1b2cbd2dd851452306beed0ab9bdaeeab1cc8ad46f84b47cd81eeaff6dd6b8"}, + {file = "fastavro-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29e9baee0b2f37ecd09bde3b487cf900431fd548c85be3e4fe1b9a0b2a917f1"}, + {file = "fastavro-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:66e132c710663230292bc63e2cb79cf95b16ccb94a5fc99bb63694b24e312fc5"}, + {file = "fastavro-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:38aca63ce604039bcdf2edd14912d00287bdbf8b76f9aa42b28e6ca0bf950092"}, + {file = "fastavro-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9787835f6449ee94713e7993a700432fce3763024791ffa8a58dc91ef9d1f950"}, + {file = "fastavro-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536cb448bc83811056be02749fd9df37a69621678f02597d272970a769e9b40c"}, + {file = "fastavro-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e9d5027cf7d9968f8f819958b41bfedb933323ea6d6a0485eefacaa1afd91f54"}, + {file = "fastavro-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:792adfc0c80c7f1109e0ab4b0decef20691fdf0a45091d397a0563872eb56d42"}, + {file = "fastavro-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:650b22766259f7dd7519dfa4e4658f0e233c319efa130b9cf0c36a500e09cc57"}, + {file = "fastavro-1.8.2.tar.gz", hash = "sha256:ab9d9226d4b66b6b3d0661a57cd45259b0868fed1c0cd4fac95249b9e0973320"}, +] + +[package.extras] +codecs = ["lz4", "python-snappy", "zstandard"] +lz4 = ["lz4"] +snappy = ["python-snappy"] +zstandard = ["zstandard"] + +[[package]] +name = "feast" +version = "0.31.1" +description = "Python SDK for Feast" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "feast-0.31.1-py2.py3-none-any.whl", hash = "sha256:ffe9d88972ad9a2d25e152893a0c6bb11508aa19c801aebc8f628c7567b9db91"}, + {file = "feast-0.31.1.tar.gz", hash = "sha256:efd2eb43d1d1f66ac2436d02c0fabb116cb4fdcebc475da5223acd760843e56b"}, +] + +[package.dependencies] +bowler = "*" +click = ">=7.0.0,<9.0.0" +colorama = ">=0.3.9,<1" +dask = ">=2021.1.0" +dill = ">=0.3.0,<0.4.0" +fastapi = ">=0.68.0,<1" +fastavro = ">=1.1.0,<2" +grpcio = ">=1.47.0,<2" +grpcio-reflection = ">=1.47.0,<2" +hiredis = {version = ">=2.0.0,<3", optional = true, markers = "extra == \"redis\""} +httpx = ">=0.23.3" +Jinja2 = ">=2,<4" +jsonschema = "*" +mmh3 = "*" +numpy = ">=1.22,<3" +pandas = ">=1.4.3,<2" +pandavro = ">=1.5.0,<1.6.0" +proto-plus = ">=1.20.0,<2" +protobuf = ">3.20,<5" +pyarrow = ">=4,<12" +pydantic = ">=1,<2" +pygments = ">=2.12.0,<3" +PyYAML = ">=5.4.0,<7" +redis = {version = "4.2.2", optional = true, markers = "extra == \"redis\""} +requests = "*" +snowflake-connector-python = {version = ">=3,<4", extras = ["pandas"], optional = true, markers = "extra == \"snowflake\""} +SQLAlchemy = {version = ">1,<2", extras = ["mypy"]} +tabulate = ">=0.8.0,<1" +tenacity = ">=7,<9" +toml = ">=0.10.0,<1" +tqdm = ">=4,<5" +typeguard = "2.13.3" +uvicorn = {version = ">=0.14.0,<1", extras = ["standard"]} + +[package.extras] +aws = ["boto3 (>=1.17.0,<2)", "docker (>=5.0.2)"] +azure = ["SQLAlchemy (>=1.4.19)", "azure-identity (>=1.6.1)", "azure-storage-blob (>=0.37.0)", "pymssql", "pyodbc (>=4.0.30)"] +bytewax = ["bytewax (==0.15.1)", "docker (>=5.0.2)", "kubernetes (<=20.13.0)"] +cassandra = ["cassandra-driver (>=3.24.0,<4)"] +ci = ["SQLAlchemy (>=1.4.19)", "Sphinx (>4.0.0,<7)", "adlfs (==0.5.9)", "assertpy (==1.1)", "avro (==1.10.0)", "azure-identity (>=1.6.1)", "azure-storage-blob (>=0.37.0)", "black (>=22.6.0,<23)", "boto3 (>=1.17.0,<2)", "build", "bytewax (==0.15.1)", "cassandra-driver (>=3.24.0,<4)", "cryptography (>=35.0,<36)", "docker (>=5.0.2)", "firebase-admin (>=5.2.0,<6)", "flake8", "gcsfs (>=0.4.0,<=2022.01.0)", "google-api-core (>=1.23.0,<3)", "google-cloud-bigquery-storage (>=2.0.0,<3)", "google-cloud-bigquery[pandas] (>=2,<4)", "google-cloud-bigtable (>=2.11.0,<3)", "google-cloud-datastore (>=2.1.0,<3)", "google-cloud-storage (>=1.34.0,<3)", "googleapis-common-protos (>=1.52.0,<2)", "great-expectations (>=0.15.41,<0.16.0)", "grpcio-testing (>=1.47.0)", "grpcio-tools (>=1.47.0)", "happybase (>=1.2.0,<3)", "hazelcast-python-client (>=5.1)", "hiredis (>=2.0.0,<3)", "isort (>=5,<6)", "kubernetes (<=20.13.0)", "minio (==7.1.0)", "mock (==2.0.0)", "moto", "mypy (>=0.981,<0.990)", "mypy-protobuf (==3.1)", "mysqlclient", "pip-tools", "pre-commit", "psutil (==5.9.0)", "psycopg2-binary (>=2.8.3,<3)", "py (>=1.11.0)", "pybindgen", "pymssql", "pymysql", "pyodbc (>=4.0.30)", "pyspark (>=3.0.0,<4)", "pytest (>=6.0.0,<8)", "pytest-benchmark (>=3.4.1,<4)", "pytest-cov", "pytest-lazy-fixture (==0.6.3)", "pytest-mock (==1.10.4)", "pytest-ordering (>=0.6.0,<0.7.0)", "pytest-timeout (==1.4.2)", "pytest-xdist", "redis (==4.2.2)", "regex", "rockset (>=1.0.3)", "snowflake-connector-python[pandas] (>=3,<4)", "testcontainers (>=3.5,<4)", "trino (>=0.305.0,<0.400.0)", "types-PyMySQL", "types-PyYAML", "types-protobuf (>=3.19.22,<3.20.0)", "types-python-dateutil", "types-pytz", "types-redis", "types-requests", "types-setuptools", "types-tabulate", "urllib3 (>=1.25.4,<2)"] +dev = ["SQLAlchemy (>=1.4.19)", "Sphinx (>4.0.0,<7)", "adlfs (==0.5.9)", "assertpy (==1.1)", "avro (==1.10.0)", "azure-identity (>=1.6.1)", "azure-storage-blob (>=0.37.0)", "black (>=22.6.0,<23)", "boto3 (>=1.17.0,<2)", "build", "bytewax (==0.15.1)", "cassandra-driver (>=3.24.0,<4)", "cryptography (>=35.0,<36)", "docker (>=5.0.2)", "firebase-admin (>=5.2.0,<6)", "flake8", "gcsfs (>=0.4.0,<=2022.01.0)", "google-api-core (>=1.23.0,<3)", "google-cloud-bigquery-storage (>=2.0.0,<3)", "google-cloud-bigquery[pandas] (>=2,<4)", "google-cloud-bigtable (>=2.11.0,<3)", "google-cloud-datastore (>=2.1.0,<3)", "google-cloud-storage (>=1.34.0,<3)", "googleapis-common-protos (>=1.52.0,<2)", "great-expectations (>=0.15.41,<0.16.0)", "grpcio-testing (>=1.0,<2.0)", "grpcio-testing (>=1.47.0)", "grpcio-tools (>=1.47.0)", "happybase (>=1.2.0,<3)", "hazelcast-python-client (>=5.1)", "hiredis (>=2.0.0,<3)", "isort (>=5,<6)", "kubernetes (<=20.13.0)", "minio (==7.1.0)", "mock (==2.0.0)", "moto", "mypy (>=0.981,<0.990)", "mypy-protobuf (==3.1)", "mysqlclient", "pip-tools", "pre-commit", "psutil (==5.9.0)", "psycopg2-binary (>=2.8.3,<3)", "py (>=1.11.0)", "pybindgen", "pymssql", "pymysql", "pyodbc (>=4.0.30)", "pyspark (>=3.0.0,<4)", "pytest (>=6.0.0,<8)", "pytest-benchmark (>=3.4.1,<4)", "pytest-cov", "pytest-lazy-fixture (==0.6.3)", "pytest-mock (==1.10.4)", "pytest-ordering (>=0.6.0,<0.7.0)", "pytest-timeout (==1.4.2)", "pytest-xdist", "redis (==4.2.2)", "regex", "rockset (>=1.0.3)", "snowflake-connector-python[pandas] (>=3,<4)", "testcontainers (>=3.5,<4)", "trino (>=0.305.0,<0.400.0)", "types-PyMySQL", "types-PyYAML", "types-protobuf (>=3.19.22,<3.20.0)", "types-python-dateutil", "types-pytz", "types-redis", "types-requests", "types-setuptools", "types-tabulate", "urllib3 (>=1.25.4,<2)"] +docs = ["SQLAlchemy (>=1.4.19)", "Sphinx (>4.0.0,<7)", "adlfs (==0.5.9)", "assertpy (==1.1)", "avro (==1.10.0)", "azure-identity (>=1.6.1)", "azure-storage-blob (>=0.37.0)", "black (>=22.6.0,<23)", "boto3 (>=1.17.0,<2)", "build", "bytewax (==0.15.1)", "cassandra-driver (>=3.24.0,<4)", "cryptography (>=35.0,<36)", "docker (>=5.0.2)", "firebase-admin (>=5.2.0,<6)", "flake8", "gcsfs (>=0.4.0,<=2022.01.0)", "google-api-core (>=1.23.0,<3)", "google-cloud-bigquery-storage (>=2.0.0,<3)", "google-cloud-bigquery[pandas] (>=2,<4)", "google-cloud-bigtable (>=2.11.0,<3)", "google-cloud-datastore (>=2.1.0,<3)", "google-cloud-storage (>=1.34.0,<3)", "googleapis-common-protos (>=1.52.0,<2)", "great-expectations (>=0.15.41,<0.16.0)", "grpcio-testing (>=1.47.0)", "grpcio-tools (>=1.47.0)", "happybase (>=1.2.0,<3)", "hazelcast-python-client (>=5.1)", "hiredis (>=2.0.0,<3)", "isort (>=5,<6)", "kubernetes (<=20.13.0)", "minio (==7.1.0)", "mock (==2.0.0)", "moto", "mypy (>=0.981,<0.990)", "mypy-protobuf (==3.1)", "pip-tools", "pre-commit", "psutil (==5.9.0)", "psycopg2-binary (>=2.8.3,<3)", "py (>=1.11.0)", "pybindgen", "pymssql", "pyodbc (>=4.0.30)", "pyspark (>=3.0.0,<4)", "pytest (>=6.0.0,<8)", "pytest-benchmark (>=3.4.1,<4)", "pytest-cov", "pytest-lazy-fixture (==0.6.3)", "pytest-mock (==1.10.4)", "pytest-ordering (>=0.6.0,<0.7.0)", "pytest-timeout (==1.4.2)", "pytest-xdist", "redis (==4.2.2)", "regex", "rockset (>=1.0.3)", "snowflake-connector-python[pandas] (>=3,<4)", "testcontainers (>=3.5,<4)", "trino (>=0.305.0,<0.400.0)", "types-PyYAML", "types-protobuf (>=3.19.22,<3.20.0)", "types-python-dateutil", "types-pytz", "types-redis", "types-requests", "types-setuptools", "types-tabulate", "urllib3 (>=1.25.4,<2)"] +gcp = ["google-api-core (>=1.23.0,<3)", "google-cloud-bigquery-storage (>=2.0.0,<3)", "google-cloud-bigquery[pandas] (>=2,<4)", "google-cloud-bigtable (>=2.11.0,<3)", "google-cloud-datastore (>=2.1.0,<3)", "google-cloud-storage (>=1.34.0,<3)", "googleapis-common-protos (>=1.52.0,<2)"] +ge = ["great-expectations (>=0.15.41,<0.16.0)"] +hazelcast = ["hazelcast-python-client (>=5.1)"] +hbase = ["happybase (>=1.2.0,<3)"] +mysql = ["mysqlclient", "pymysql", "types-PyMySQL"] +postgres = ["psycopg2-binary (>=2.8.3,<3)"] +redis = ["hiredis (>=2.0.0,<3)", "redis (==4.2.2)"] +rockset = ["rockset (>=1.0.3)"] +snowflake = ["snowflake-connector-python[pandas] (>=3,<4)"] +spark = ["pyspark (>=3.0.0,<4)"] +trino = ["regex", "trino (>=0.305.0,<0.400.0)"] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "fissix" +version = "21.11.13" +description = "Monkeypatches to override default behavior of lib2to3." +optional = false +python-versions = ">=3.6" +files = [ + {file = "fissix-21.11.13-py3-none-any.whl", hash = "sha256:55cd7541944bd15cfd8b0b2117fd63b84dcce5e3780ff7f77894059a87e5243a"}, + {file = "fissix-21.11.13.tar.gz", hash = "sha256:496a51b86afb4c64cf15cfed38a0f3a371f300b298c962c881e33620f076a514"}, +] + +[package.dependencies] +appdirs = "*" + +[[package]] +name = "fsspec" +version = "2023.6.0" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2023.6.0-py3-none-any.whl", hash = "sha256:1cbad1faef3e391fba6dc005ae9b5bdcbf43005c9167ce78c915549c352c869a"}, + {file = "fsspec-2023.6.0.tar.gz", hash = "sha256:d0b2f935446169753e7a5c5c55681c54ea91996cc67be93c39a154fb3a2742af"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "requests"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + +[[package]] +name = "greenlet" +version = "2.0.2" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +files = [ + {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, + {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, + {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, + {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, + {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, + {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, + {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, + {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, + {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, + {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, + {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, + {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, + {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, + {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, + {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, + {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, + {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, + {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, + {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, + {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, + {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, + {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, +] + +[package.extras] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] + +[[package]] +name = "grpcio" +version = "1.56.2" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.7" +files = [ + {file = "grpcio-1.56.2-cp310-cp310-linux_armv7l.whl", hash = "sha256:bf0b9959e673505ee5869950642428046edb91f99942607c2ecf635f8a4b31c9"}, + {file = "grpcio-1.56.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:5144feb20fe76e73e60c7d73ec3bf54f320247d1ebe737d10672480371878b48"}, + {file = "grpcio-1.56.2-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:a72797549935c9e0b9bc1def1768c8b5a709538fa6ab0678e671aec47ebfd55e"}, + {file = "grpcio-1.56.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3f3237a57e42f79f1e560726576aedb3a7ef931f4e3accb84ebf6acc485d316"}, + {file = "grpcio-1.56.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:900bc0096c2ca2d53f2e5cebf98293a7c32f532c4aeb926345e9747452233950"}, + {file = "grpcio-1.56.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:97e0efaebbfd222bcaac2f1735c010c1d3b167112d9d237daebbeedaaccf3d1d"}, + {file = "grpcio-1.56.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c0c85c5cbe8b30a32fa6d802588d55ffabf720e985abe9590c7c886919d875d4"}, + {file = "grpcio-1.56.2-cp310-cp310-win32.whl", hash = "sha256:06e84ad9ae7668a109e970c7411e7992751a116494cba7c4fb877656527f9a57"}, + {file = "grpcio-1.56.2-cp310-cp310-win_amd64.whl", hash = "sha256:10954662f77dc36c9a1fb5cc4a537f746580d6b5734803be1e587252682cda8d"}, + {file = "grpcio-1.56.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:c435f5ce1705de48e08fcbcfaf8aee660d199c90536e3e06f2016af7d6a938dd"}, + {file = "grpcio-1.56.2-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:6108e5933eb8c22cd3646e72d5b54772c29f57482fd4c41a0640aab99eb5071d"}, + {file = "grpcio-1.56.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:8391cea5ce72f4a12368afd17799474015d5d3dc00c936a907eb7c7eaaea98a5"}, + {file = "grpcio-1.56.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:750de923b456ca8c0f1354d6befca45d1f3b3a789e76efc16741bd4132752d95"}, + {file = "grpcio-1.56.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fda2783c12f553cdca11c08e5af6eecbd717280dc8fbe28a110897af1c15a88c"}, + {file = "grpcio-1.56.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9e04d4e4cfafa7c5264e535b5d28e786f0571bea609c3f0aaab13e891e933e9c"}, + {file = "grpcio-1.56.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89a49cc5ad08a38b6141af17e00d1dd482dc927c7605bc77af457b5a0fca807c"}, + {file = "grpcio-1.56.2-cp311-cp311-win32.whl", hash = "sha256:6a007a541dff984264981fbafeb052bfe361db63578948d857907df9488d8774"}, + {file = "grpcio-1.56.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4063ef2b11b96d949dccbc5a987272f38d55c23c4c01841ea65a517906397f"}, + {file = "grpcio-1.56.2-cp37-cp37m-linux_armv7l.whl", hash = "sha256:a6ff459dac39541e6a2763a4439c4ca6bc9ecb4acc05a99b79246751f9894756"}, + {file = "grpcio-1.56.2-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:f20fd21f7538f8107451156dd1fe203300b79a9ddceba1ee0ac8132521a008ed"}, + {file = "grpcio-1.56.2-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:d1fbad1f9077372b6587ec589c1fc120b417b6c8ad72d3e3cc86bbbd0a3cee93"}, + {file = "grpcio-1.56.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee26e9dfb3996aff7c870f09dc7ad44a5f6732b8bdb5a5f9905737ac6fd4ef1"}, + {file = "grpcio-1.56.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c60abd950d6de3e4f1ddbc318075654d275c29c846ab6a043d6ed2c52e4c8c"}, + {file = "grpcio-1.56.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1c31e52a04e62c8577a7bf772b3e7bed4df9c9e0dd90f92b6ffa07c16cab63c9"}, + {file = "grpcio-1.56.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:345356b307cce5d14355e8e055b4ca5f99bc857c33a3dc1ddbc544fca9cd0475"}, + {file = "grpcio-1.56.2-cp37-cp37m-win_amd64.whl", hash = "sha256:42e63904ee37ae46aa23de50dac8b145b3596f43598fa33fe1098ab2cbda6ff5"}, + {file = "grpcio-1.56.2-cp38-cp38-linux_armv7l.whl", hash = "sha256:7c5ede2e2558f088c49a1ddda19080e4c23fb5d171de80a726b61b567e3766ed"}, + {file = "grpcio-1.56.2-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:33971197c47965cc1d97d78d842163c283e998223b151bab0499b951fd2c0b12"}, + {file = "grpcio-1.56.2-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:d39f5d4af48c138cb146763eda14eb7d8b3ccbbec9fe86fb724cd16e0e914c64"}, + {file = "grpcio-1.56.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded637176addc1d3eef35331c39acc598bac550d213f0a1bedabfceaa2244c87"}, + {file = "grpcio-1.56.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c90da4b124647547a68cf2f197174ada30c7bb9523cb976665dfd26a9963d328"}, + {file = "grpcio-1.56.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3ccb621749a81dc7755243665a70ce45536ec413ef5818e013fe8dfbf5aa497b"}, + {file = "grpcio-1.56.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4eb37dd8dd1aa40d601212afa27ca5be255ba792e2e0b24d67b8af5e012cdb7d"}, + {file = "grpcio-1.56.2-cp38-cp38-win32.whl", hash = "sha256:ddb4a6061933bd9332b74eac0da25f17f32afa7145a33a0f9711ad74f924b1b8"}, + {file = "grpcio-1.56.2-cp38-cp38-win_amd64.whl", hash = "sha256:8940d6de7068af018dfa9a959a3510e9b7b543f4c405e88463a1cbaa3b2b379a"}, + {file = "grpcio-1.56.2-cp39-cp39-linux_armv7l.whl", hash = "sha256:51173e8fa6d9a2d85c14426bdee5f5c4a0654fd5fddcc21fe9d09ab0f6eb8b35"}, + {file = "grpcio-1.56.2-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:373b48f210f43327a41e397391715cd11cfce9ded2fe76a5068f9bacf91cc226"}, + {file = "grpcio-1.56.2-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:42a3bbb2bc07aef72a7d97e71aabecaf3e4eb616d39e5211e2cfe3689de860ca"}, + {file = "grpcio-1.56.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5344be476ac37eb9c9ad09c22f4ea193c1316bf074f1daf85bddb1b31fda5116"}, + {file = "grpcio-1.56.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3fa3ab0fb200a2c66493828ed06ccd1a94b12eddbfb985e7fd3e5723ff156c6"}, + {file = "grpcio-1.56.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b975b85d1d5efc36cf8b237c5f3849b64d1ba33d6282f5e991f28751317504a1"}, + {file = "grpcio-1.56.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cbdf2c498e077282cd427cfd88bdce4668019791deef0be8155385ab2ba7837f"}, + {file = "grpcio-1.56.2-cp39-cp39-win32.whl", hash = "sha256:139f66656a762572ae718fa0d1f2dce47c05e9fbf7a16acd704c354405b97df9"}, + {file = "grpcio-1.56.2-cp39-cp39-win_amd64.whl", hash = "sha256:830215173ad45d670140ff99aac3b461f9be9a6b11bee1a17265aaaa746a641a"}, + {file = "grpcio-1.56.2.tar.gz", hash = "sha256:0ff789ae7d8ddd76d2ac02e7d13bfef6fc4928ac01e1dcaa182be51b6bcc0aaa"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.56.2)"] + +[[package]] +name = "grpcio-reflection" +version = "1.56.2" +description = "Standard Protobuf Reflection Service for gRPC" +optional = false +python-versions = ">=3.6" +files = [ + {file = "grpcio-reflection-1.56.2.tar.gz", hash = "sha256:74a81766af639ab8f1b7f59531dc814640f4a1bcf012dc06ce6be205b50a394c"}, + {file = "grpcio_reflection-1.56.2-py3-none-any.whl", hash = "sha256:004bcc3d4a3dcd89bf83253e4c08ca032fc7ca862f7532ea09ebdf08801fc193"}, +] + +[package.dependencies] +grpcio = ">=1.56.2" +protobuf = ">=4.21.6" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "hiredis" +version = "2.2.3" +description = "Python wrapper for hiredis" +optional = false +python-versions = ">=3.7" +files = [ + {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:9a1a80a8fa767f2fdc3870316a54b84fe9fc09fa6ab6a2686783de6a228a4604"}, + {file = "hiredis-2.2.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3f006c28c885deb99b670a5a66f367a175ab8955b0374029bad7111f5357dcd4"}, + {file = "hiredis-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffaf841546905d90ff189de7397aa56413b1ce5e54547f17a98f0ebf3a3b0a3b"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cadb0ac7ba3babfd804e425946bec9717b320564a1390f163a54af9365a720a"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33bc4721632ef9708fa44e5df0066053fccc8e65410a2c48573192517a533b48"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:227c5b4bcb60f89008c275d596e4a7b6625a6b3c827b8a66ae582eace7051f71"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61995eb826009d99ed8590747bc0da683a5f4fbb4faa8788166bf3810845cd5c"}, + {file = "hiredis-2.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f969edc851efe23010e0f53a64269f2629a9364135e9ec81c842e8b2277d0c1"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27e560eefb57914d742a837f1da98d3b29cb22eff013c8023b7cf52ae6e051d"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3759f4789ae1913b7df278dfc9e8749205b7a106f888cd2903d19461e24a7697"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c6cb613148422c523945cdb8b6bed617856f2602fd8750e33773ede2616e55d5"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:1d274d5c511dfc03f83f997d3238eaa9b6ee3f982640979f509373cced891e98"}, + {file = "hiredis-2.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b7fe075e91b9d9cff40eba4fb6a8eff74964d3979a39be9a9ef58b1b4cb3604"}, + {file = "hiredis-2.2.3-cp310-cp310-win32.whl", hash = "sha256:77924b0d32fd1f493d3df15d9609ddf9d94c31a364022a6bf6b525ce9da75bea"}, + {file = "hiredis-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:dcb0569dd5bfe6004658cd0f229efa699a3169dcb4f77bd72e188adda302063d"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:d115790f18daa99b5c11a506e48923b630ef712e9e4b40482af942c3d40638b8"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c3b8be557e08b234774925622e196f0ee36fe4eab66cd19df934d3efd8f3743"}, + {file = "hiredis-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f5446068197b35a11ccc697720c41879c8657e2e761aaa8311783aac84cef20"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa17a3b22b3726d54d7af20394f65d4a1735a842a4e0f557dc67a90f6965c4bc"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7df645b6b7800e8b748c217fbd6a4ca8361bcb9a1ae6206cc02377833ec8a1aa"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fb9300959a0048138791f3d68359d61a788574ec9556bddf1fec07f2dbc5320"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d7e459fe7313925f395148d36d9b7f4f8dac65be06e45d7af356b187cef65fc"}, + {file = "hiredis-2.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8eceffca3941775b646cd585cd19b275d382de43cc3327d22f7c75d7b003d481"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b17baf702c6e5b4bb66e1281a3efbb1d749c9d06cdb92b665ad81e03118f78fc"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e43e2b5acaad09cf48c032f7e4926392bb3a3f01854416cf6d82ebff94d5467"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a7205497d7276a81fe92951a29616ef96562ed2f91a02066f72b6f93cb34b40e"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:126623b03c31cb6ac3e0d138feb6fcc36dd43dd34fc7da7b7a0c38b5d75bc896"}, + {file = "hiredis-2.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:071c5814b850574036506a8118034f97c3cbf2fe9947ff45a27b07a48da56240"}, + {file = "hiredis-2.2.3-cp311-cp311-win32.whl", hash = "sha256:d1be9e30e675f5bc1cb534633324578f6f0944a1bcffe53242cf632f554f83b6"}, + {file = "hiredis-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9a7c987e161e3c58f992c63b7e26fea7fe0777f3b975799d23d65bbb8cb5899"}, + {file = "hiredis-2.2.3-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f2dcb8389fa3d453927b1299f46bdb38473c293c8269d5c777d33ea0e526b610"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2df98f5e071320c7d84e8bd07c0542acdd0a7519307fc31774d60e4b842ec4f"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a72e4a523cdfc521762137559c08dfa360a3caef63620be58c699d1717dac1"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9b9e5bde7030cae83aa900b5bd660decc65afd2db8c400f3c568c815a47ca2a"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2614f17e261f72efc2f19f5e5ff2ee19e2296570c0dcf33409e22be30710de"}, + {file = "hiredis-2.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46525fbd84523cac75af5bf524bc74aaac848beaf31b142d2df8a787d9b4bbc4"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1a4ce40ba11da9382c14da31f4f9e88c18f7d294f523decd0fadfb81f51ad18"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cda592405bbd29d53942e0389dc3fa77b49c362640210d7e94a10c14a677d4d"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5e6674a017629284ef373b50496d9fb1a89b85a20a7fa100ecd109484ec748e5"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:e62ec131816c6120eff40dffe43424e140264a15fa4ab88c301bd6a595913af3"}, + {file = "hiredis-2.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17e938d9d3ee92e1adbff361706f1c36cc60eeb3e3eeca7a3a353eae344f4c91"}, + {file = "hiredis-2.2.3-cp37-cp37m-win32.whl", hash = "sha256:95d2305fd2a7b179cacb48b10f618872fc565c175f9f62b854e8d1acac3e8a9e"}, + {file = "hiredis-2.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8f9dbe12f011a9b784f58faecc171d22465bb532c310bd588d769ba79a59ef5a"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:5a4bcef114fc071d5f52c386c47f35aae0a5b43673197b9288a15b584da8fa3a"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:232d0a70519865741ba56e1dfefd160a580ae78c30a1517bad47b3cf95a3bc7d"}, + {file = "hiredis-2.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9076ce8429785c85f824650735791738de7143f61f43ae9ed83e163c0ca0fa44"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec58fb7c2062f835595c12f0f02dcda76d0eb0831423cc191d1e18c9276648de"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2b34a6444b8f9c1e9f84bd2c639388e5d14f128afd14a869dfb3d9af893aa2"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:818dfd310aa1020a13cd08ee48e116dd8c3bb2e23b8161f8ac4df587dd5093d7"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d9ea6c8d4cbdeee2e0d43379ce2881e4af0454b00570677c59f33f2531cd38"}, + {file = "hiredis-2.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1eadbcd3de55ac42310ff82550d3302cb4efcd4e17d76646a17b6e7004bb42b"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:477c34c4489666dc73cb5e89dafe2617c3e13da1298917f73d55aac4696bd793"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:14824e457e4f5cda685c3345d125da13949bcf3bb1c88eb5d248c8d2c3dee08f"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9cd32326dfa6ce87edf754153b0105aca64486bebe93b9600ccff74fa0b224df"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:51341e70b467004dcbec3a6ce8c478d2d6241e0f6b01e4c56764afd5022e1e9d"}, + {file = "hiredis-2.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2443659c76b226267e2a04dbbb21bc2a3f91aa53bdc0c22964632753ae43a247"}, + {file = "hiredis-2.2.3-cp38-cp38-win32.whl", hash = "sha256:4e3e3e31423f888d396b1fc1f936936e52af868ac1ec17dd15e3eeba9dd4de24"}, + {file = "hiredis-2.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:20f509e3a1a20d6e5f5794fc37ceb21f70f409101fcfe7a8bde783894d51b369"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:d20891e3f33803b26d54c77fd5745878497091e33f4bbbdd454cf6e71aee8890"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:50171f985e17970f87d5a29e16603d1e5b03bdbf5c2691a37e6c912942a6b657"}, + {file = "hiredis-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9944a2cac25ffe049a7e89f306e11b900640837d1ef38d9be0eaa4a4e2b73a52"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a5c8019ff94988d56eb49b15de76fe83f6b42536d76edeb6565dbf7fe14b973"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a286ded34eb16501002e3713b3130c987366eee2ba0d58c33c72f27778e31676"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b3e974ad15eb32b1f537730dea70b93a4c3db7b026de3ad2b59da49c6f7454d"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08415ea74c1c29b9d6a4ca3dd0e810dc1af343c1d1d442e15ba133b11ab5be6a"}, + {file = "hiredis-2.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e17d04ea58ab8cf3f2dc52e875db16077c6357846006780086fff3189fb199d"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6ccdcb635dae85b006592f78e32d97f4bc7541cb27829d505f9c7fefcef48298"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69536b821dd1bc78058a6e7541743f8d82bf2d981b91280b14c4daa6cdc7faba"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3753df5f873d473f055e1f8837bfad0bd3b277c86f3c9bf058c58f14204cd901"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6f88cafe46612b6fa68e6dea49e25bebf160598bba00101caa51cc8c1f18d597"}, + {file = "hiredis-2.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33ee3ea5cad3a8cb339352cd230b411eb437a2e75d7736c4899acab32056ccdb"}, + {file = "hiredis-2.2.3-cp39-cp39-win32.whl", hash = "sha256:b4f3d06dc16671b88a13ae85d8ca92534c0b637d59e49f0558d040a691246422"}, + {file = "hiredis-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4f674e309cd055ee7a48304ceb8cf43265d859faf4d7d01d270ce45e976ae9d3"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8f280ab4e043b089777b43b4227bdc2035f88da5072ab36588e0ccf77d45d058"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c2a551f3b8a26f7940d6ee10b837810201754b8d7e6f6b1391655370882c5a"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c4e3c258eafaab21b174b17270a0cc093718d61cdbde8c03f85ec4bf835343"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc36a9dded458d4e37492fe3e619c6c83caae794d26ad925adbce61d592f8428"}, + {file = "hiredis-2.2.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4ed68a3b1ccb4313d2a42546fd7e7439ad4745918a48b6c9bcaa61e1e3e42634"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3bf4b5bae472630c229518e4a814b1b68f10a3d9b00aeaec45f1a330f03a0251"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33a94d264e6e12a79d9bb8af333b01dc286b9f39c99072ab5fef94ce1f018e17"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fa6811a618653164f918b891a0fa07052bd71a799defa5c44d167cac5557b26"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af33f370be90b48bbaf0dab32decbdcc522b1fa95d109020a963282086518a8e"}, + {file = "hiredis-2.2.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b9953d87418ac228f508d93898ab572775e4d3b0eeb886a1a7734553bcdaf291"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5e7bb4dd524f50b71c20ef5a12bd61da9b463f8894b18a06130942fe31509881"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89a258424158eb8b3ed9f65548d68998da334ef155d09488c5637723eb1cd697"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4a65276f6ecdebe75f2a53f578fbc40e8d2860658420d5e0611c56bbf5054c"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:334f2738700b20faa04a0d813366fb16ed17287430a6b50584161d5ad31ca6d7"}, + {file = "hiredis-2.2.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d194decd9608f11c777946f596f31d5aacad13972a0a87829ae1e6f2d26c1885"}, + {file = "hiredis-2.2.3.tar.gz", hash = "sha256:e75163773a309e56a9b58165cf5a50e0f84b755f6ff863b2c01a38918fe92daa"}, +] + +[[package]] +name = "httpcore" +version = "0.17.3" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, + {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httptools" +version = "0.6.0" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.5.0" +files = [ + {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:818325afee467d483bfab1647a72054246d29f9053fd17cc4b86cda09cc60339"}, + {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72205730bf1be875003692ca54a4a7c35fac77b4746008966061d9d41a61b0f5"}, + {file = "httptools-0.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33eb1d4e609c835966e969a31b1dedf5ba16b38cab356c2ce4f3e33ffa94cad3"}, + {file = "httptools-0.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdc6675ec6cb79d27e0575750ac6e2b47032742e24eed011b8db73f2da9ed40"}, + {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:463c3bc5ef64b9cf091be9ac0e0556199503f6e80456b790a917774a616aff6e"}, + {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f228b88b0e8c6099a9c4757ce9fdbb8b45548074f8d0b1f0fc071e35655d1c"}, + {file = "httptools-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:0781fedc610293a2716bc7fa142d4c85e6776bc59d617a807ff91246a95dea35"}, + {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:721e503245d591527cddd0f6fd771d156c509e831caa7a57929b55ac91ee2b51"}, + {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:274bf20eeb41b0956e34f6a81f84d26ed57c84dd9253f13dcb7174b27ccd8aaf"}, + {file = "httptools-0.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:259920bbae18740a40236807915def554132ad70af5067e562f4660b62c59b90"}, + {file = "httptools-0.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfd2ae8a2d532952ac54445a2fb2504c804135ed28b53fefaf03d3a93eb1fd"}, + {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f959e4770b3fc8ee4dbc3578fd910fab9003e093f20ac8c621452c4d62e517cb"}, + {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e22896b42b95b3237eccc42278cd72c0df6f23247d886b7ded3163452481e38"}, + {file = "httptools-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:38f3cafedd6aa20ae05f81f2e616ea6f92116c8a0f8dcb79dc798df3356836e2"}, + {file = "httptools-0.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:47043a6e0ea753f006a9d0dd076a8f8c99bc0ecae86a0888448eb3076c43d717"}, + {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a541579bed0270d1ac10245a3e71e5beeb1903b5fbbc8d8b4d4e728d48ff1d"}, + {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d802e7b2538a9756df5acc062300c160907b02e15ed15ba035b02bce43e89c"}, + {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:26326e0a8fe56829f3af483200d914a7cd16d8d398d14e36888b56de30bec81a"}, + {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e41ccac9e77cd045f3e4ee0fc62cbf3d54d7d4b375431eb855561f26ee7a9ec4"}, + {file = "httptools-0.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e748fc0d5c4a629988ef50ac1aef99dfb5e8996583a73a717fc2cac4ab89932"}, + {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cf8169e839a0d740f3d3c9c4fa630ac1a5aaf81641a34575ca6773ed7ce041a1"}, + {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5dcc14c090ab57b35908d4a4585ec5c0715439df07be2913405991dbb37e049d"}, + {file = "httptools-0.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0b0571806a5168013b8c3d180d9f9d6997365a4212cb18ea20df18b938aa0b"}, + {file = "httptools-0.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb4a608c631f7dcbdf986f40af7a030521a10ba6bc3d36b28c1dc9e9035a3c0"}, + {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:93f89975465133619aea8b1952bc6fa0e6bad22a447c6d982fc338fbb4c89649"}, + {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:73e9d66a5a28b2d5d9fbd9e197a31edd02be310186db423b28e6052472dc8201"}, + {file = "httptools-0.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:22c01fcd53648162730a71c42842f73b50f989daae36534c818b3f5050b54589"}, + {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f96d2a351b5625a9fd9133c95744e8ca06f7a4f8f0b8231e4bbaae2c485046a"}, + {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72ec7c70bd9f95ef1083d14a755f321d181f046ca685b6358676737a5fecd26a"}, + {file = "httptools-0.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b703d15dbe082cc23266bf5d9448e764c7cb3fcfe7cb358d79d3fd8248673ef9"}, + {file = "httptools-0.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c723ed5982f8ead00f8e7605c53e55ffe47c47465d878305ebe0082b6a1755"}, + {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0a816bb425c116a160fbc6f34cece097fd22ece15059d68932af686520966bd"}, + {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dea66d94e5a3f68c5e9d86e0894653b87d952e624845e0b0e3ad1c733c6cc75d"}, + {file = "httptools-0.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:23b09537086a5a611fad5696fc8963d67c7e7f98cb329d38ee114d588b0b74cd"}, + {file = "httptools-0.6.0.tar.gz", hash = "sha256:9fc6e409ad38cbd68b177cd5158fc4042c796b82ca88d99ec78f07bed6c6b796"}, +] + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "httpx" +version = "0.24.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.18.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "importlib-metadata" +version = "6.8.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, + {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "importlib-resources" +version = "6.0.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.0.0-py3-none-any.whl", hash = "sha256:d952faee11004c045f785bb5636e8f885bed30dc3c940d5d42798a2a4541c185"}, + {file = "importlib_resources-6.0.0.tar.gz", hash = "sha256:4cf94875a8368bd89531a756df9a9ebe1f150e0f885030b461237bc7f2d905f2"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipykernel" +version = "6.25.0" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.25.0-py3-none-any.whl", hash = "sha256:f0042e867ac3f6bca1679e6a88cbd6a58ed93a44f9d0866aecde6efe8de76659"}, + {file = "ipykernel-6.25.0.tar.gz", hash = "sha256:e342ce84712861be4b248c4a73472be4702c1b0dd77448bfd6bcfb3af9d5ddf9"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=20" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.12.2" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipython-8.12.2-py3-none-any.whl", hash = "sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc"}, + {file = "ipython-8.12.2.tar.gz", hash = "sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jaraco-classes" +version = "3.3.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"}, + {file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[[package]] +name = "jedi" +version = "0.19.0" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, + {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsonschema" +version = "4.18.6" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.18.6-py3-none-any.whl", hash = "sha256:dc274409c36175aad949c68e5ead0853aaffbe8e88c830ae66bb3c7a1728ad2d"}, + {file = "jsonschema-4.18.6.tar.gz", hash = "sha256:ce71d2f8c7983ef75a756e568317bf54bc531dc3ad7e66a128eae0d51623d8a3"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +jsonschema-specifications = ">=2023.03.6" +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.7.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, + {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, +] + +[package.dependencies] +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +referencing = ">=0.28.0" + +[[package]] +name = "jupyter-client" +version = "8.3.0" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.3.0-py3-none-any.whl", hash = "sha256:7441af0c0672edc5d28035e92ba5e32fadcfa8a4e608a434c228836a89df6158"}, + {file = "jupyter_client-8.3.0.tar.gz", hash = "sha256:3af69921fe99617be1670399a0b857ad67275eefcfa291e2c81a160b7b650f5f"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.3.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.3.1-py3-none-any.whl", hash = "sha256:ae9036db959a71ec1cac33081eeb040a79e681f08ab68b0883e9a676c7a90dce"}, + {file = "jupyter_core-5.3.1.tar.gz", hash = "sha256:5ba5c7938a7f97a6b0481463f7ff0dbac7c15ba48cf46fa4035ca6e838aa1aba"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "keyring" +version = "24.2.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-24.2.0-py3-none-any.whl", hash = "sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6"}, + {file = "keyring-24.2.0.tar.gz", hash = "sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +"jaraco.classes" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +completion = ["shtab"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[[package]] +name = "locket" +version = "1.0.0" +description = "File-based locks for Python on Linux and Windows" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3"}, + {file = "locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632"}, +] + +[[package]] +name = "lz4" +version = "4.3.2" +description = "LZ4 Bindings for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "lz4-4.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1c4c100d99eed7c08d4e8852dd11e7d1ec47a3340f49e3a96f8dfbba17ffb300"}, + {file = "lz4-4.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:edd8987d8415b5dad25e797043936d91535017237f72fa456601be1479386c92"}, + {file = "lz4-4.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7c50542b4ddceb74ab4f8b3435327a0861f06257ca501d59067a6a482535a77"}, + {file = "lz4-4.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5614d8229b33d4a97cb527db2a1ac81308c6e796e7bdb5d1309127289f69d5"}, + {file = "lz4-4.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f00a9ba98f6364cadda366ae6469b7b3568c0cced27e16a47ddf6b774169270"}, + {file = "lz4-4.3.2-cp310-cp310-win32.whl", hash = "sha256:b10b77dc2e6b1daa2f11e241141ab8285c42b4ed13a8642495620416279cc5b2"}, + {file = "lz4-4.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:86480f14a188c37cb1416cdabacfb4e42f7a5eab20a737dac9c4b1c227f3b822"}, + {file = "lz4-4.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7c2df117def1589fba1327dceee51c5c2176a2b5a7040b45e84185ce0c08b6a3"}, + {file = "lz4-4.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1f25eb322eeb24068bb7647cae2b0732b71e5c639e4e4026db57618dcd8279f0"}, + {file = "lz4-4.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8df16c9a2377bdc01e01e6de5a6e4bbc66ddf007a6b045688e285d7d9d61d1c9"}, + {file = "lz4-4.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f571eab7fec554d3b1db0d666bdc2ad85c81f4b8cb08906c4c59a8cad75e6e22"}, + {file = "lz4-4.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7211dc8f636ca625abc3d4fb9ab74e5444b92df4f8d58ec83c8868a2b0ff643d"}, + {file = "lz4-4.3.2-cp311-cp311-win32.whl", hash = "sha256:867664d9ca9bdfce840ac96d46cd8838c9ae891e859eb98ce82fcdf0e103a947"}, + {file = "lz4-4.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:a6a46889325fd60b8a6b62ffc61588ec500a1883db32cddee9903edfba0b7584"}, + {file = "lz4-4.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a85b430138882f82f354135b98c320dafb96fc8fe4656573d95ab05de9eb092"}, + {file = "lz4-4.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d5c93f8badacfa0456b660285e394e65023ef8071142e0dcbd4762166e1be0"}, + {file = "lz4-4.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b50f096a6a25f3b2edca05aa626ce39979d63c3b160687c8c6d50ac3943d0ba"}, + {file = "lz4-4.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:200d05777d61ba1ff8d29cb51c534a162ea0b4fe6d3c28be3571a0a48ff36080"}, + {file = "lz4-4.3.2-cp37-cp37m-win32.whl", hash = "sha256:edc2fb3463d5d9338ccf13eb512aab61937be50aa70734bcf873f2f493801d3b"}, + {file = "lz4-4.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:83acfacab3a1a7ab9694333bcb7950fbeb0be21660d236fd09c8337a50817897"}, + {file = "lz4-4.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a9eec24ec7d8c99aab54de91b4a5a149559ed5b3097cf30249b665689b3d402"}, + {file = "lz4-4.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31d72731c4ac6ebdce57cd9a5cabe0aecba229c4f31ba3e2c64ae52eee3fdb1c"}, + {file = "lz4-4.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83903fe6db92db0be101acedc677aa41a490b561567fe1b3fe68695b2110326c"}, + {file = "lz4-4.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926b26db87ec8822cf1870efc3d04d06062730ec3279bbbd33ba47a6c0a5c673"}, + {file = "lz4-4.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e05afefc4529e97c08e65ef92432e5f5225c0bb21ad89dee1e06a882f91d7f5e"}, + {file = "lz4-4.3.2-cp38-cp38-win32.whl", hash = "sha256:ad38dc6a7eea6f6b8b642aaa0683253288b0460b70cab3216838747163fb774d"}, + {file = "lz4-4.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:7e2dc1bd88b60fa09b9b37f08553f45dc2b770c52a5996ea52b2b40f25445676"}, + {file = "lz4-4.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:edda4fb109439b7f3f58ed6bede59694bc631c4b69c041112b1b7dc727fffb23"}, + {file = "lz4-4.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ca83a623c449295bafad745dcd399cea4c55b16b13ed8cfea30963b004016c9"}, + {file = "lz4-4.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5ea0e788dc7e2311989b78cae7accf75a580827b4d96bbaf06c7e5a03989bd5"}, + {file = "lz4-4.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a98b61e504fb69f99117b188e60b71e3c94469295571492a6468c1acd63c37ba"}, + {file = "lz4-4.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4931ab28a0d1c133104613e74eec1b8bb1f52403faabe4f47f93008785c0b929"}, + {file = "lz4-4.3.2-cp39-cp39-win32.whl", hash = "sha256:ec6755cacf83f0c5588d28abb40a1ac1643f2ff2115481089264c7630236618a"}, + {file = "lz4-4.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:4caedeb19e3ede6c7a178968b800f910db6503cb4cb1e9cc9221157572139b49"}, + {file = "lz4-4.3.2.tar.gz", hash = "sha256:e1431d84a9cfb23e6773e72078ce8e65cad6745816d4cbf9ae67da5ea419acda"}, +] + +[package.extras] +docs = ["sphinx (>=1.6.0)", "sphinx-bootstrap-theme"] +flake8 = ["flake8"] +tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mmh3" +version = "4.0.1" +description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." +optional = false +python-versions = "*" +files = [ + {file = "mmh3-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b719ba87232749095011d567a36a25e40ed029fc61c47e74a12416d8bb60b311"}, + {file = "mmh3-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f0ad423711c5096cf4a346011f3b3ec763208e4f4cc4b10ed41cad2a03dbfaed"}, + {file = "mmh3-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80918e3f8ab6b717af0a388c14ffac5a89c15d827ff008c1ef545b8b32724116"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8222cd5f147defa1355b4042d590c34cef9b2bb173a159fcb72cda204061a4ac"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3821bcd1961ef19247c78c5d01b5a759de82ab0c023e2ff1d5ceed74322fa018"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59f7ed28c24249a54665f1ed3f6c7c1c56618473381080f79bcc0bd1d1db2e4a"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dacd8d07d4b9be8f0cb6e8fd9a08fc237c18578cf8d42370ee8af2f5a2bf1967"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd00883ef6bcf7831026ce42e773a4b2a4f3a7bf9003a4e781fecb1144b06c1"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df73d1c7f0c50c0f8061cd349968fd9dcc6a9e7592d1c834fa898f9c98f8dd7e"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f41eeae98f15af0a4ba2a92bce11d8505b612012af664a7634bbfdba7096f5fc"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ce9bb622e9f1162cafd033071b32ac495c5e8d5863fca2a5144c092a0f129a5b"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dd92e0ff9edee6af960d9862a3e519d651e6344321fd280fb082654fc96ecc4d"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aefa8ac8c8fc8ad93365477baef2125dbfd7235880a9c47dca2c46a0af49ef7"}, + {file = "mmh3-4.0.1-cp310-cp310-win32.whl", hash = "sha256:a076ea30ec279a63f44f4c203e4547b5710d00581165fed12583d2017139468d"}, + {file = "mmh3-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5aa1e87e448ee1ffa3737b72f2fe3f5960159ab75bbac2f49dca6fb9797132f6"}, + {file = "mmh3-4.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:45155ff2f291c3a1503d1c93e539ab025a13fd8b3f2868650140702b8bd7bfc2"}, + {file = "mmh3-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:91f81d6dd4d0c3b4235b4a58a545493c946669c751a2e0f15084171dc2d81fee"}, + {file = "mmh3-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbfddaf55207798f5b29341e5b3a24dbff91711c51b1665eabc9d910255a78f0"}, + {file = "mmh3-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0deb8e19121c0896fdc709209aceda30a367cda47f4a884fcbe56223dbf9e867"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df468ac7b61ec7251d7499e27102899ca39d87686f659baf47f84323f8f4541f"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84936c113814c6ef3bc4bd3d54f538d7ba312d1d0c2441ac35fdd7d5221c60f6"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b1df3cf5ce5786aa093f45462118d87ff485f0d69699cdc34f6289b1e833632"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da281aa740aa9e7f9bebb879c1de0ea9366687ece5930f9f5027e7c87d018153"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ec380933a56eb9fea16d7fcd49f1b5a5c92d7d2b86f25e9a845b72758ee8c42"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2fa905fcec8a30e1c0ef522afae1d6170c4f08e6a88010a582f67c59209fb7c7"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9b23a06315a65ef0b78da0be32409cfce0d6d83e51d70dcebd3302a61e4d34ce"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:36c27089b12026db14be594d750f7ea6d5d785713b40a971b063f033f5354a74"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:6338341ae6fa5eaa46f69ed9ac3e34e8eecad187b211a6e552e0d8128c568eb1"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1aece29e27d0c8fb489d00bb712fba18b4dd10e39c9aec2e216c779ae6400b8f"}, + {file = "mmh3-4.0.1-cp311-cp311-win32.whl", hash = "sha256:2733e2160c142eed359e25e5529915964a693f0d043165b53933f904a731c1b3"}, + {file = "mmh3-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:09f9f643e0b7f8d98473efdfcdb155105824a38a1ada374625b84c1208197a9b"}, + {file = "mmh3-4.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:d93422f38bc9c4d808c5438a011b769935a87df92ce277e9e22b6ec0ae8ed2e2"}, + {file = "mmh3-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:41013c033dc446d3bfb573621b8b53223adcfcf07be1da0bcbe166d930276882"}, + {file = "mmh3-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be46540eac024dd8d9b82899d35b2f23592d3d3850845aba6f10e6127d93246b"}, + {file = "mmh3-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e64114b30c6c1e30f8201433b5fa6108a74a5d6f1a14af1b041360c0dd056aa"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:275637ecca755565e3b0505d3ecf8e1e0a51eb6a3cbe6e212ed40943f92f98cd"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:955178c8e8d3bc9ad18eab443af670cd13fe18a6b2dba16db2a2a0632be8a133"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:750afe0477e0c17904611045ad311ff10bc6c2ec5f5ddc5dd949a2b9bf71d5d5"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b7c18c35e9d6a59d6c5f94a6576f800ff2b500e41cd152ecfc7bb4330f32ba2"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b8635b1fc6b25d93458472c5d682a1a4b9e6c53e7f4ca75d2bf2a18fa9363ae"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:057b8de47adee8ad0f2e194ffa445b9845263c1c367ddb335e9ae19c011b25cc"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:78c0ee0197cfc912f57172aa16e784ad55b533e2e2e91b3a65188cc66fbb1b6e"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d6acb15137467592691e41e6f897db1d2823ff3283111e316aa931ac0b5a5709"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f91b2598e1f25e013da070ff641a29ebda76292d3a7bdd20ef1736e9baf0de67"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a78f6f2592395321e2f0dc6b618773398b2c9b15becb419364e0960df53e9f04"}, + {file = "mmh3-4.0.1-cp38-cp38-win32.whl", hash = "sha256:d8650982d0b70af24700bd32b15fab33bb3ef9be4af411100f4960a938b0dd0f"}, + {file = "mmh3-4.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:2489949c7261870a02eeaa2ec7b966881c1775df847c8ce6ea4de3e9d96b5f4f"}, + {file = "mmh3-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dcd03a4bb0fa3db03648d26fb221768862f089b6aec5272f0df782a8b4fe5b5b"}, + {file = "mmh3-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3775fb0cc675977e5b506b12b8f23cd220be3d4c2d4db7df81f03c9f61baa4cc"}, + {file = "mmh3-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f250f78328d41cdf73d3ad9809359636f4fb7a846d7a6586e1a0f0d2f5f2590"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4161009c9077d5ebf8b472dbf0f41b9139b3d380e0bbe71bf9b503efb2965584"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cf986ebf530717fefeee8d0decbf3f359812caebba985e2c8885c0ce7c2ee4e"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b55741ed51e928b1eec94a119e003fa3bc0139f4f9802e19bea3af03f7dd55a"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8250375641b8c5ce5d56a00c6bb29f583516389b8bde0023181d5eba8aa4119"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29373e802bc094ffd490e39047bac372ac893c0f411dac3223ef11775e34acd0"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:071ba41e56f5c385d13ee84b288ccaf46b70cd9e9a6d8cbcbe0964dee68c0019"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:909e0b88d2c6285481fa6895c2a0faf6384e1b0093f72791aa57d1e04f4adc65"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:51d356f4380f9d9c2a0612156c3d1e7359933991e84a19304440aa04fd723e68"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c4b2549949efa63d8decb6572f7e75fad4f2375d52fafced674323239dd9812d"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bcc7b32a89c4e5c6fdef97d82e8087ba26a20c25b4aaf0723abd0b302525934"}, + {file = "mmh3-4.0.1-cp39-cp39-win32.whl", hash = "sha256:8edee21ae4f4337fb970810ef5a263e5d2212b85daca0d39daf995e13380e908"}, + {file = "mmh3-4.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8cbb6f90f08952fcc90dbf08f0310fdf4d61096c5cb7db8adf03e23f3b857ae5"}, + {file = "mmh3-4.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:ce71856cbca9d7c74d084eeee1bc5b126ed197c1c9530a4fdb994d099b9bc4db"}, + {file = "mmh3-4.0.1.tar.gz", hash = "sha256:ad8be695dc4e44a79631748ba5562d803f0ac42d36a6b97a53aca84a70809385"}, +] + +[package.extras] +test = ["mypy (>=1.0)", "pytest (>=7.0.0)"] + +[[package]] +name = "more-itertools" +version = "9.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.7" +files = [ + {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"}, + {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"}, +] + +[[package]] +name = "moreorless" +version = "0.4.0" +description = "Python diff wrapper" +optional = false +python-versions = ">=3.6" +files = [ + {file = "moreorless-0.4.0-py2.py3-none-any.whl", hash = "sha256:17f1fbef60fd21c84ee085a929fe3acefcaddca30df5dd09c024e9939a9e6a00"}, + {file = "moreorless-0.4.0.tar.gz", hash = "sha256:85e19972c1a0b3a49f8543914f57bd83f6e1b10df144d5b97b8c5e9744d9c08c"}, +] + +[package.dependencies] +click = "*" + +[[package]] +name = "msgspec" +version = "0.16.0" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgspec-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f06692e976bbb89d1c1eb95109679195a4ec172fbec73dee5027af1450f46b59"}, + {file = "msgspec-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:77b19814d7206542927c46e0c7807955739c181fef71f973e96c4e47a14c5893"}, + {file = "msgspec-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0cda74ffda2b2757eadf2259f8a68a5321f4fb8423bff26fa9e28eaaf8720d6"}, + {file = "msgspec-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddebe801459cd6f67e4279b3c679dc731729fabf64f42d7a4bd567ca3eb56377"}, + {file = "msgspec-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c0458cf8a44f630d372348d95b3b536a52412d4e61a53a3f3f31f070c95eb461"}, + {file = "msgspec-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ce21b56ecb462abb5291863c2e29dc58177da3c8f43f3d0edf69009daca05b66"}, + {file = "msgspec-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbd657cc2e2840f86a75c1fee265854835e2196d12502a64ce1390239cca58a9"}, + {file = "msgspec-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7cdfad50f388d1c1a933d9239913cb3bd993d4b631011df34d893fb3011971e0"}, + {file = "msgspec-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a4f641e10d4ef70a77184c002ec1512c0b83ddbb6c21314c85f9507c029b997"}, + {file = "msgspec-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9075d40d7739228b6158969239ad7708f483bbd4e8eb09c92c95c6062b470617"}, + {file = "msgspec-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f710d8b1992cf0690c9feeebd741d69c3627bace3f16e09e8556d65eb012fe"}, + {file = "msgspec-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1b6412c20bd687df6fb7c72a8a1bbc1a5da1be948bc01ce3d21e645263cddb6c"}, + {file = "msgspec-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e1a6708408cd5a44e39aa268086fe0992001e5881282c178a158af86727ddfa3"}, + {file = "msgspec-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:ccd842d25593fbff6505e77e0a3701c89bf3a1c260247e4e541e4e58dc81a6cc"}, + {file = "msgspec-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dbc137f037c2cb4ee731ef5066d3cb85a639b5d805df7f4c96aaefd914c7c5af"}, + {file = "msgspec-0.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:611c90eff0e2dd19b53e93bf8040450404f262aa05eee27089c8d29e92031db6"}, + {file = "msgspec-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e177b0f05f05321e415d42a30f854df47452c973e18957899410163da5c88c"}, + {file = "msgspec-0.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db3b2e32be73d155525e0886764963379eef8d6f7a16da6cd023516aed01ee"}, + {file = "msgspec-0.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa4bb83597ad8fce23b53ff16acd7931a55bf4ee2197c0282f077e5caacd5ee2"}, + {file = "msgspec-0.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:15c64bbefd34a5beb0da9eb22bff3ba0aab296f9828084998cd7716b5c1e2964"}, + {file = "msgspec-0.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:4111ab4373c185df543248d86eeb885c623319f82f4256164617beb8fbfa5071"}, + {file = "msgspec-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24809b66ef632f1ae91af7d281dd78eec2f516ad9963b3e9e61cb7b34495875d"}, + {file = "msgspec-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f14e3b1df80967aef772c9ac083df56ecf067f7cad7d291180f2733449e83a5"}, + {file = "msgspec-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78f3a914a356daf334f9dc7e72fb55025b39c65b6fcec507b18cdca7e65b97f6"}, + {file = "msgspec-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4830b073860e05d2cf1ef56d610035402f83b129a2742032ef2492d093385ef"}, + {file = "msgspec-0.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fea1bc172bd07a427ee538169b6447433dee018624f1e43ab7d046ccfbffb66f"}, + {file = "msgspec-0.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d80d2c1a8e3ee2b991d7fcf8d8b0904cb4fa68fe4d5abf8739453cffdde418c4"}, + {file = "msgspec-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:21095c687ae624a812a13a7e5d4ea6f3039c8768052ac0fb2818b8744779872a"}, + {file = "msgspec-0.16.0.tar.gz", hash = "sha256:0a3d5441cc8bda37957a1edb52c6f6ff4fcfebcaf20c771ad4cd4eade75c0f1a"}, +] + +[package.extras] +dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] +doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] +test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] +toml = ["tomli", "tomli-w"] +yaml = ["pyyaml"] + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nest-asyncio" +version = "1.5.7" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.5.7-py3-none-any.whl", hash = "sha256:5301c82941b550b3123a1ea772ba9a1c80bad3a182be8c1a5ae6ad3be57a9657"}, + {file = "nest_asyncio-1.5.7.tar.gz", hash = "sha256:6a80f7b98f24d9083ed24608977c09dd608d83f91cccc24c9d2cba6d10e01c10"}, +] + +[[package]] +name = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.19.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_api-1.19.0-py3-none-any.whl", hash = "sha256:dcd2a0ad34b691964947e1d50f9e8c415c32827a1d87f0459a72deb9afdf5597"}, + {file = "opentelemetry_api-1.19.0.tar.gz", hash = "sha256:db374fb5bea00f3c7aa290f5d94cea50b659e6ea9343384c5f6c2bb5d5e8db65"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<7.0" + +[[package]] +name = "oscrypto" +version = "1.3.0" +description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." +optional = false +python-versions = "*" +files = [ + {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, + {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, +] + +[package.dependencies] +asn1crypto = ">=1.5.1" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pandas" +version = "1.5.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, + {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, + {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, + {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, + {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, + {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, + {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, + {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, + {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, + {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, + {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, + {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, + {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, + {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, + {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, + {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.20.3", markers = "python_version < \"3.10\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, +] +python-dateutil = ">=2.8.1" +pytz = ">=2020.1" + +[package.extras] +test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] + +[[package]] +name = "pandavro" +version = "1.5.2" +description = "The interface between Avro and pandas DataFrame" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pandavro-1.5.2.tar.gz", hash = "sha256:fd4ede1600cfc08150f28a7a1c1dd4abfbc87d76cd4a5b1639c8f55a04e97914"}, +] + +[package.dependencies] +fastavro = ">=0.14.11" +numpy = ">=1.7.0" +pandas = "*" +six = ">=1.9" + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "partd" +version = "1.4.0" +description = "Appendable key-value storage" +optional = false +python-versions = ">=3.7" +files = [ + {file = "partd-1.4.0-py3-none-any.whl", hash = "sha256:7a63529348cf0dff14b986db641cd1b83c16b5cb9fc647c2851779db03282ef8"}, + {file = "partd-1.4.0.tar.gz", hash = "sha256:aa0ff35dbbcc807ae374db56332f4c1b39b46f67bf2975f5151e0b4186aed0d5"}, +] + +[package.dependencies] +locket = "*" +toolz = "*" + +[package.extras] +complete = ["blosc", "numpy (>=1.9.0)", "pandas (>=0.19.0)", "pyzmq"] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + +[[package]] +name = "pip" +version = "23.2.1" +description = "The PyPA recommended tool for installing Python packages." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pip-23.2.1-py3-none-any.whl", hash = "sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be"}, + {file = "pip-23.2.1.tar.gz", hash = "sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2"}, +] + +[[package]] +name = "pip-tools" +version = "6.14.0" +description = "pip-tools keeps your pinned dependencies fresh." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pip-tools-6.14.0.tar.gz", hash = "sha256:06366be0e08d86b416407333e998b4d305d5bd925151b08942ed149380ba3e47"}, + {file = "pip_tools-6.14.0-py3-none-any.whl", hash = "sha256:c5ad042cd27c0b343b10db1db7f77a7d087beafbec59ae6df1bba4d3368dfe8c"}, +] + +[package.dependencies] +build = "*" +click = ">=8" +pip = ">=22.2" +setuptools = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} +wheel = "*" + +[package.extras] +coverage = ["covdefaults", "pytest-cov"] +testing = ["flit-core (>=2,<4)", "poetry-core (>=1.0.0)", "pytest (>=7.2.0)", "pytest-rerunfailures", "pytest-xdist", "tomli-w"] + +[[package]] +name = "pkginfo" +version = "1.9.6" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, + {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov"] + +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +description = "Resolve a name to an object." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + +[[package]] +name = "platformdirs" +version = "3.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.39" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "proto-plus" +version = "1.22.3" +description = "Beautiful, Pythonic protocol buffers." +optional = false +python-versions = ">=3.6" +files = [ + {file = "proto-plus-1.22.3.tar.gz", hash = "sha256:fdcd09713cbd42480740d2fe29c990f7fbd885a67efc328aa8be6ee3e9f76a6b"}, + {file = "proto_plus-1.22.3-py3-none-any.whl", hash = "sha256:a49cd903bc0b6ab41f76bf65510439d56ca76f868adf0274e738bfdd096894df"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<5.0.0dev" + +[package.extras] +testing = ["google-api-core[grpc] (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "4.23.4" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "protobuf-4.23.4-cp310-abi3-win32.whl", hash = "sha256:5fea3c64d41ea5ecf5697b83e41d09b9589e6f20b677ab3c48e5f242d9b7897b"}, + {file = "protobuf-4.23.4-cp310-abi3-win_amd64.whl", hash = "sha256:7b19b6266d92ca6a2a87effa88ecc4af73ebc5cfde194dc737cf8ef23a9a3b12"}, + {file = "protobuf-4.23.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8547bf44fe8cec3c69e3042f5c4fb3e36eb2a7a013bb0a44c018fc1e427aafbd"}, + {file = "protobuf-4.23.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a"}, + {file = "protobuf-4.23.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:effeac51ab79332d44fba74660d40ae79985901ac21bca408f8dc335a81aa597"}, + {file = "protobuf-4.23.4-cp37-cp37m-win32.whl", hash = "sha256:c3e0939433c40796ca4cfc0fac08af50b00eb66a40bbbc5dee711998fb0bbc1e"}, + {file = "protobuf-4.23.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9053df6df8e5a76c84339ee4a9f5a2661ceee4a0dab019e8663c50ba324208b0"}, + {file = "protobuf-4.23.4-cp38-cp38-win32.whl", hash = "sha256:e1c915778d8ced71e26fcf43c0866d7499891bca14c4368448a82edc61fdbc70"}, + {file = "protobuf-4.23.4-cp38-cp38-win_amd64.whl", hash = "sha256:351cc90f7d10839c480aeb9b870a211e322bf05f6ab3f55fcb2f51331f80a7d2"}, + {file = "protobuf-4.23.4-cp39-cp39-win32.whl", hash = "sha256:6dd9b9940e3f17077e820b75851126615ee38643c2c5332aa7a359988820c720"}, + {file = "protobuf-4.23.4-cp39-cp39-win_amd64.whl", hash = "sha256:0a5759f5696895de8cc913f084e27fd4125e8fb0914bb729a17816a33819f474"}, + {file = "protobuf-4.23.4-py3-none-any.whl", hash = "sha256:e9d0be5bf34b275b9f87ba7407796556abeeba635455d036c7351f7c183ef8ff"}, + {file = "protobuf-4.23.4.tar.gz", hash = "sha256:ccd9430c0719dce806b93f89c91de7977304729e55377f872a92465d548329a9"}, +] + +[[package]] +name = "psutil" +version = "5.9.5" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, + {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, + {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, + {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, + {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, + {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, + {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, + {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, + {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, + {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, + {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, + {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, + {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, + {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pyarrow" +version = "10.0.1" +description = "Python library for Apache Arrow" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyarrow-10.0.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e00174764a8b4e9d8d5909b6d19ee0c217a6cf0232c5682e31fdfbd5a9f0ae52"}, + {file = "pyarrow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f7a7dbe2f7f65ac1d0bd3163f756deb478a9e9afc2269557ed75b1b25ab3610"}, + {file = "pyarrow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb627673cb98708ef00864e2e243f51ba7b4c1b9f07a1d821f98043eccd3f585"}, + {file = "pyarrow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba71e6fc348c92477586424566110d332f60d9a35cb85278f42e3473bc1373da"}, + {file = "pyarrow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4ede715c004b6fc535de63ef79fa29740b4080639a5ff1ea9ca84e9282f349"}, + {file = "pyarrow-10.0.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:e3fe5049d2e9ca661d8e43fab6ad5a4c571af12d20a57dffc392a014caebef65"}, + {file = "pyarrow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:254017ca43c45c5098b7f2a00e995e1f8346b0fb0be225f042838323bb55283c"}, + {file = "pyarrow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70acca1ece4322705652f48db65145b5028f2c01c7e426c5d16a30ba5d739c24"}, + {file = "pyarrow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb57334f2c57979a49b7be2792c31c23430ca02d24becd0b511cbe7b6b08649"}, + {file = "pyarrow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1765a18205eb1e02ccdedb66049b0ec148c2a0cb52ed1fb3aac322dfc086a6ee"}, + {file = "pyarrow-10.0.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:61f4c37d82fe00d855d0ab522c685262bdeafd3fbcb5fe596fe15025fbc7341b"}, + {file = "pyarrow-10.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e141a65705ac98fa52a9113fe574fdaf87fe0316cde2dffe6b94841d3c61544c"}, + {file = "pyarrow-10.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf26f809926a9d74e02d76593026f0aaeac48a65b64f1bb17eed9964bfe7ae1a"}, + {file = "pyarrow-10.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:443eb9409b0cf78df10ced326490e1a300205a458fbeb0767b6b31ab3ebae6b2"}, + {file = "pyarrow-10.0.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f2d00aa481becf57098e85d99e34a25dba5a9ade2f44eb0b7d80c80f2984fc03"}, + {file = "pyarrow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b1fc226d28c7783b52a84d03a66573d5a22e63f8a24b841d5fc68caeed6784d4"}, + {file = "pyarrow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa59933b20183c1c13efc34bd91efc6b2997377c4c6ad9272da92d224e3beb1"}, + {file = "pyarrow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:668e00e3b19f183394388a687d29c443eb000fb3fe25599c9b4762a0afd37775"}, + {file = "pyarrow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1bc6e4d5d6f69e0861d5d7f6cf4d061cf1069cb9d490040129877acf16d4c2a"}, + {file = "pyarrow-10.0.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:42ba7c5347ce665338f2bc64685d74855900200dac81a972d49fe127e8132f75"}, + {file = "pyarrow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b069602eb1fc09f1adec0a7bdd7897f4d25575611dfa43543c8b8a75d99d6874"}, + {file = "pyarrow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fb4a0c12a2ac1ed8e7e2aa52aade833772cf2d3de9dde685401b22cec30002"}, + {file = "pyarrow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0c5986bf0808927f49640582d2032a07aa49828f14e51f362075f03747d198"}, + {file = "pyarrow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0ec7587d759153f452d5263dbc8b1af318c4609b607be2bd5127dcda6708cdb1"}, + {file = "pyarrow-10.0.1.tar.gz", hash = "sha256:1a14f57a5f472ce8234f2964cd5184cccaa8df7e04568c64edc33b23eb285dd5"}, +] + +[package.dependencies] +numpy = ">=1.16.6" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pycryptodomex" +version = "3.18.0" +description = "Cryptographic library for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycryptodomex-3.18.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:160a39a708c36fa0b168ab79386dede588e62aec06eb505add870739329aecc6"}, + {file = "pycryptodomex-3.18.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c2953afebf282a444c51bf4effe751706b4d0d63d7ca2cc51db21f902aa5b84e"}, + {file = "pycryptodomex-3.18.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:ba95abd563b0d1b88401658665a260852a8e6c647026ee6a0a65589287681df8"}, + {file = "pycryptodomex-3.18.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:192306cf881fe3467dda0e174a4f47bb3a8bb24b90c9cdfbdc248eec5fc0578c"}, + {file = "pycryptodomex-3.18.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:f9ab5ef0718f6a8716695dea16d83b671b22c45e9c0c78fd807c32c0192e54b5"}, + {file = "pycryptodomex-3.18.0-cp27-cp27m-win32.whl", hash = "sha256:50308fcdbf8345e5ec224a5502b4215178bdb5e95456ead8ab1a69ffd94779cb"}, + {file = "pycryptodomex-3.18.0-cp27-cp27m-win_amd64.whl", hash = "sha256:4d9379c684efea80fdab02a3eb0169372bca7db13f9332cb67483b8dc8b67c37"}, + {file = "pycryptodomex-3.18.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5594a125dae30d60e94f37797fc67ce3c744522de7992c7c360d02fdb34918f8"}, + {file = "pycryptodomex-3.18.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:8ff129a5a0eb5ff16e45ca4fa70a6051da7f3de303c33b259063c19be0c43d35"}, + {file = "pycryptodomex-3.18.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:3d9314ac785a5b75d5aaf924c5f21d6ca7e8df442e5cf4f0fefad4f6e284d422"}, + {file = "pycryptodomex-3.18.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:f237278836dda412a325e9340ba2e6a84cb0f56b9244781e5b61f10b3905de88"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac614363a86cc53d8ba44b6c469831d1555947e69ab3276ae8d6edc219f570f7"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:302a8f37c224e7b5d72017d462a2be058e28f7be627bdd854066e16722d0fc0c"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:6421d23d6a648e83ba2670a352bcd978542dad86829209f59d17a3f087f4afef"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84e105787f5e5d36ec6a581ff37a1048d12e638688074b2a00bcf402f9aa1c2"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6875eb8666f68ddbd39097867325bd22771f595b4e2b0149739b5623c8bf899b"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:27072a494ce621cc7a9096bbf60ed66826bb94db24b49b7359509e7951033e74"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1949e09ea49b09c36d11a951b16ff2a05a0ffe969dda1846e4686ee342fe8646"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6ed3606832987018615f68e8ed716a7065c09a0fe94afd7c9ca1b6777f0ac6eb"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-win32.whl", hash = "sha256:d56c9ec41258fd3734db9f5e4d2faeabe48644ba9ca23b18e1839b3bdf093222"}, + {file = "pycryptodomex-3.18.0-cp35-abi3-win_amd64.whl", hash = "sha256:e00a4bacb83a2627e8210cb353a2e31f04befc1155db2976e5e239dd66482278"}, + {file = "pycryptodomex-3.18.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2dc4eab20f4f04a2d00220fdc9258717b82d31913552e766d5f00282c031b70a"}, + {file = "pycryptodomex-3.18.0-pp27-pypy_73-win32.whl", hash = "sha256:75672205148bdea34669173366df005dbd52be05115e919551ee97171083423d"}, + {file = "pycryptodomex-3.18.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bec6c80994d4e7a38312072f89458903b65ec99bed2d65aa4de96d997a53ea7a"}, + {file = "pycryptodomex-3.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d35a8ffdc8b05e4b353ba281217c8437f02c57d7233363824e9d794cf753c419"}, + {file = "pycryptodomex-3.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f0a46bee539dae4b3dfe37216f678769349576b0080fdbe431d19a02da42ff"}, + {file = "pycryptodomex-3.18.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:71687eed47df7e965f6e0bf3cadef98f368d5221f0fb89d2132effe1a3e6a194"}, + {file = "pycryptodomex-3.18.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:73d64b32d84cf48d9ec62106aa277dbe99ab5fbfd38c5100bc7bddd3beb569f7"}, + {file = "pycryptodomex-3.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbdcce0a226d9205560a5936b05208c709b01d493ed8307792075dedfaaffa5f"}, + {file = "pycryptodomex-3.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58fc0aceb9c961b9897facec9da24c6a94c5db04597ec832060f53d4d6a07196"}, + {file = "pycryptodomex-3.18.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:215be2980a6b70704c10796dd7003eb4390e7be138ac6fb8344bf47e71a8d470"}, + {file = "pycryptodomex-3.18.0.tar.gz", hash = "sha256:3e3ecb5fe979e7c1bb0027e518340acf7ee60415d79295e5251d13c68dde576e"}, +] + +[[package]] +name = "pydantic" +version = "1.10.12" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, + {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, + {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, + {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, + {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, + {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, + {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, + {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, + {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, + {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pyhumps" +version = "3.8.0" +description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node" +optional = false +python-versions = "*" +files = [ + {file = "pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6"}, + {file = "pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3"}, +] + +[[package]] +name = "pyinstrument" +version = "4.5.1" +description = "Call stack profiler for Python. Shows you why your code is slow!" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyinstrument-4.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f334250b158010d1e2c70d9d10b880f848e03a917079b366b1e2d8890348d41"}, + {file = "pyinstrument-4.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:55537cd763aee8bce65a201d5ec1aef74677d9ff3ab3391316604ca68740d92a"}, + {file = "pyinstrument-4.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d7933bd83e913e21c4031d5c1aeeb2483147e4037363f43475df9ad962c748"}, + {file = "pyinstrument-4.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0d8f6b6df7ce338af35b213cd89b685b2a7c15569f482476c4e0942700b3e71"}, + {file = "pyinstrument-4.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98101d064b7af008189dd6f0bdd01f9be39bc6a4630505dfb13ff6ef51a0c67c"}, + {file = "pyinstrument-4.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46f1607e29f93da16d38be41ad2062a56731ff4efa24e561ac848719e8b8ca41"}, + {file = "pyinstrument-4.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e287ebc1a8b00d3a767829c03f210df0824ab2e0f6340e8f63bab6fcef1b3546"}, + {file = "pyinstrument-4.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d15613b8d5d509c29001f2edfadd73d418c2814262433fd1225c4f7893e4010a"}, + {file = "pyinstrument-4.5.1-cp310-cp310-win32.whl", hash = "sha256:04c67f08bac41173bc6b44396c60bf1a1879864d0684a7717b1bb8be27793bd9"}, + {file = "pyinstrument-4.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:dc07267447935d28ee914f955613b04d621e5bb44995f793508d6f0eb3ec2818"}, + {file = "pyinstrument-4.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8285cfb25b9ee72766bdac8db8c276755115a6e729cda4571005d1ba58c99dda"}, + {file = "pyinstrument-4.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b58239f4a0fe64f688260be0e5b4a1d19a23b890b284cf6c1c8bd0ead4616f41"}, + {file = "pyinstrument-4.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4039210a80015ae0ad2016a3b3311b068f5b334d5f5ce3c54d473f8624db0d35"}, + {file = "pyinstrument-4.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b28a4c5926036155062c83e15ca93437dbe2d41dd5feeac96f72d4d16b3431c"}, + {file = "pyinstrument-4.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89d2c2a9de60712abd2228033e4ac63cdee86783af5288f2d7f8efc365e33425"}, + {file = "pyinstrument-4.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bf0fdb17cb245c53826c77e2b95095a8fb5053e49ae8ef18aecbbd184028f9e7"}, + {file = "pyinstrument-4.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:65ac43f8a1b74a331b5a4f60985531654a8d71a7698e6be5ac7e8493e7a37f37"}, + {file = "pyinstrument-4.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:61632d287f70d850a517533b9e1bf8da41527ffc4d781d4b65106f64ee33cb98"}, + {file = "pyinstrument-4.5.1-cp311-cp311-win32.whl", hash = "sha256:22ae739152ed2366c654f80aa073579f9d5a93caffa74dcb839a62640ffe429f"}, + {file = "pyinstrument-4.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:c72a33168485172a7c2dbd6c4aa3262c8d2a6154bc0792403d8e0689c6ff5304"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8c3dabcb70b705d1342f52f0c3a00647c8a244d1e6ffe46459c05d4533ffabfc"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17d469572d48ee0b78d4ff7ed3972ff40abc70c7dab4777897c843cb03a6ab7b"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66416fa4b3413bc60e6b499e60e8d009384c85cd03535f82337dce55801c43f"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c888fca16c3ae04a6d7b5a29ee0c12f9fa23792fab695117160c48c3113428f"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:861fe8c41ac7e54a57ed6ef63268c2843fbc695012427a3d19b2eb1307d9bc61"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0bf91cd5d6c80ff25fd1a136545a5cf752522190b6e6f3806559c352f18d0e73"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b16afb5e67d4d901ef702160e85e04001183b7cdea7e38c8dfb37e491986ccff"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-win32.whl", hash = "sha256:f12312341c505e7441e5503b7c77974cff4156d072f0e7f9f822a6b5fdafbc20"}, + {file = "pyinstrument-4.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:06d96b442a1ae7c267aa34450b028d80559c4f968b10e4d3ce631b0a6ccea6ef"}, + {file = "pyinstrument-4.5.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c6234094ff0ea7d51e7d4699f192019359bf12d5bbe9e1c9c5d1983562162d58"}, + {file = "pyinstrument-4.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f025522edc35831af34bcdbe300b272b432d2afd9811eb780e326116096cbff5"}, + {file = "pyinstrument-4.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0a091c575367af427e80829ec414f69a8398acdd68ddfaeb335598071329b44"}, + {file = "pyinstrument-4.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ec169cd288f230cbc6a1773384f20481b0a14d2d7cceecf1fb65e56835eaa9a"}, + {file = "pyinstrument-4.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004745e83c79d0db7ea8787aba476f13d8bb6d00d75b00d8dbd933a9c7ee1685"}, + {file = "pyinstrument-4.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:54be442df5039bc7c73e3e86de0093ca82f3e446392bebab29e51a1512c796cb"}, + {file = "pyinstrument-4.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:35e5be8621b3381cf10b1f16bbae527cb7902e87b64e0c9706bc244f6fee51b1"}, + {file = "pyinstrument-4.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50e93fac7e42dba8b3c630ed00808e7664d0d6c6b0c477462e7b061a31be23dc"}, + {file = "pyinstrument-4.5.1-cp38-cp38-win32.whl", hash = "sha256:b0a88bfe24d4efb129ef2ae7e2d50fa29908634e893bf154e29f91655c558692"}, + {file = "pyinstrument-4.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:b8a71ef9c2ad81e5f3d5f92e1d21a0c9b5f9992e94d0bfcfa9020ea88df4e69f"}, + {file = "pyinstrument-4.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9882827e681466d1aff931479387ed77e29674c179bc10fc67f1fa96f724dd20"}, + {file = "pyinstrument-4.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:427228a011d5be21ff009dc05fcd512cee86ea2a51687a3300b8b822bad6815b"}, + {file = "pyinstrument-4.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50501756570352e78aaf2aee509b5eb6c68706a2f2701dc3a84b066e570c61ca"}, + {file = "pyinstrument-4.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6471f47860f1a5807c182be7184839d747e2702625d44ec19a8f652380541020"}, + {file = "pyinstrument-4.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59727936e862677e9716b9317e209e5e31aa1da7eb03c65083d9dee8b5fbe0f8"}, + {file = "pyinstrument-4.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9341a07885cba57c2a134847aacb629f27b4ce06a4950a4619629d35a6d8619c"}, + {file = "pyinstrument-4.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:63c27f2ae8f0501dca4d52b42285be36095f4461dd9e340d32104c2b2df3a731"}, + {file = "pyinstrument-4.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1bda9b73dde7df63d7606e37340ba0a63ad59053e59eff318f3b67d5a7ea5579"}, + {file = "pyinstrument-4.5.1-cp39-cp39-win32.whl", hash = "sha256:300ed27714c43ae2feb7572e9b3ca39660fb89b3b298e94ad24b64609f823d3c"}, + {file = "pyinstrument-4.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:f2d8e4a9a8167c2a47874d72d6ab0a4266ed484e9ae30f35a515f8594b224b51"}, + {file = "pyinstrument-4.5.1.tar.gz", hash = "sha256:b55a93be883c65650515319455636d32ab32692b097faa1e07f8cd9d4e0eeaa9"}, +] + +[package.extras] +jupyter = ["ipython"] + +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pyopenssl" +version = "23.2.0" +description = "Python wrapper module around the OpenSSL library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyOpenSSL-23.2.0-py3-none-any.whl", hash = "sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2"}, + {file = "pyOpenSSL-23.2.0.tar.gz", hash = "sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac"}, +] + +[package.dependencies] +cryptography = ">=38.0.0,<40.0.0 || >40.0.0,<40.0.1 || >40.0.1,<42" + +[package.extras] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] +test = ["flaky", "pretend", "pytest (>=3.0.1)"] + +[[package]] +name = "pyproject-hooks" +version = "1.0.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"}, + {file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-dotenv" +version = "0.5.2" +description = "A py.test plugin that parses environment files before running tests" +optional = false +python-versions = "*" +files = [ + {file = "pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732"}, + {file = "pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f"}, +] + +[package.dependencies] +pytest = ">=5.0.0" +python-dotenv = ">=0.9.1" + +[[package]] +name = "pytest-mock" +version = "3.11.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, + {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyzmq" +version = "25.1.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyzmq-25.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:1a6169e69034eaa06823da6a93a7739ff38716142b3596c180363dee729d713d"}, + {file = "pyzmq-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:19d0383b1f18411d137d891cab567de9afa609b214de68b86e20173dc624c101"}, + {file = "pyzmq-25.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1e931d9a92f628858a50f5bdffdfcf839aebe388b82f9d2ccd5d22a38a789dc"}, + {file = "pyzmq-25.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97d984b1b2f574bc1bb58296d3c0b64b10e95e7026f8716ed6c0b86d4679843f"}, + {file = "pyzmq-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:154bddda2a351161474b36dba03bf1463377ec226a13458725183e508840df89"}, + {file = "pyzmq-25.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cb6d161ae94fb35bb518b74bb06b7293299c15ba3bc099dccd6a5b7ae589aee3"}, + {file = "pyzmq-25.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:90146ab578931e0e2826ee39d0c948d0ea72734378f1898939d18bc9c823fcf9"}, + {file = "pyzmq-25.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:831ba20b660b39e39e5ac8603e8193f8fce1ee03a42c84ade89c36a251449d80"}, + {file = "pyzmq-25.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3a522510e3434e12aff80187144c6df556bb06fe6b9d01b2ecfbd2b5bfa5c60c"}, + {file = "pyzmq-25.1.0-cp310-cp310-win32.whl", hash = "sha256:be24a5867b8e3b9dd5c241de359a9a5217698ff616ac2daa47713ba2ebe30ad1"}, + {file = "pyzmq-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:5693dcc4f163481cf79e98cf2d7995c60e43809e325b77a7748d8024b1b7bcba"}, + {file = "pyzmq-25.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:13bbe36da3f8aaf2b7ec12696253c0bf6ffe05f4507985a8844a1081db6ec22d"}, + {file = "pyzmq-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:69511d604368f3dc58d4be1b0bad99b61ee92b44afe1cd9b7bd8c5e34ea8248a"}, + {file = "pyzmq-25.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a983c8694667fd76d793ada77fd36c8317e76aa66eec75be2653cef2ea72883"}, + {file = "pyzmq-25.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:332616f95eb400492103ab9d542b69d5f0ff628b23129a4bc0a2fd48da6e4e0b"}, + {file = "pyzmq-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58416db767787aedbfd57116714aad6c9ce57215ffa1c3758a52403f7c68cff5"}, + {file = "pyzmq-25.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cad9545f5801a125f162d09ec9b724b7ad9b6440151b89645241d0120e119dcc"}, + {file = "pyzmq-25.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d6128d431b8dfa888bf51c22a04d48bcb3d64431caf02b3cb943269f17fd2994"}, + {file = "pyzmq-25.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b15247c49d8cbea695b321ae5478d47cffd496a2ec5ef47131a9e79ddd7e46c"}, + {file = "pyzmq-25.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:442d3efc77ca4d35bee3547a8e08e8d4bb88dadb54a8377014938ba98d2e074a"}, + {file = "pyzmq-25.1.0-cp311-cp311-win32.whl", hash = "sha256:65346f507a815a731092421d0d7d60ed551a80d9b75e8b684307d435a5597425"}, + {file = "pyzmq-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b45d722046fea5a5694cba5d86f21f78f0052b40a4bbbbf60128ac55bfcc7b6"}, + {file = "pyzmq-25.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f45808eda8b1d71308c5416ef3abe958f033fdbb356984fabbfc7887bed76b3f"}, + {file = "pyzmq-25.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b697774ea8273e3c0460cf0bba16cd85ca6c46dfe8b303211816d68c492e132"}, + {file = "pyzmq-25.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b324fa769577fc2c8f5efcd429cef5acbc17d63fe15ed16d6dcbac2c5eb00849"}, + {file = "pyzmq-25.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5873d6a60b778848ce23b6c0ac26c39e48969823882f607516b91fb323ce80e5"}, + {file = "pyzmq-25.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f0d9e7ba6a815a12c8575ba7887da4b72483e4cfc57179af10c9b937f3f9308f"}, + {file = "pyzmq-25.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:414b8beec76521358b49170db7b9967d6974bdfc3297f47f7d23edec37329b00"}, + {file = "pyzmq-25.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:01f06f33e12497dca86353c354461f75275a5ad9eaea181ac0dc1662da8074fa"}, + {file = "pyzmq-25.1.0-cp36-cp36m-win32.whl", hash = "sha256:b5a07c4f29bf7cb0164664ef87e4aa25435dcc1f818d29842118b0ac1eb8e2b5"}, + {file = "pyzmq-25.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:968b0c737797c1809ec602e082cb63e9824ff2329275336bb88bd71591e94a90"}, + {file = "pyzmq-25.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:47b915ba666c51391836d7ed9a745926b22c434efa76c119f77bcffa64d2c50c"}, + {file = "pyzmq-25.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5af31493663cf76dd36b00dafbc839e83bbca8a0662931e11816d75f36155897"}, + {file = "pyzmq-25.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5489738a692bc7ee9a0a7765979c8a572520d616d12d949eaffc6e061b82b4d1"}, + {file = "pyzmq-25.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1fc56a0221bdf67cfa94ef2d6ce5513a3d209c3dfd21fed4d4e87eca1822e3a3"}, + {file = "pyzmq-25.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:75217e83faea9edbc29516fc90c817bc40c6b21a5771ecb53e868e45594826b0"}, + {file = "pyzmq-25.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3830be8826639d801de9053cf86350ed6742c4321ba4236e4b5568528d7bfed7"}, + {file = "pyzmq-25.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3575699d7fd7c9b2108bc1c6128641a9a825a58577775ada26c02eb29e09c517"}, + {file = "pyzmq-25.1.0-cp37-cp37m-win32.whl", hash = "sha256:95bd3a998d8c68b76679f6b18f520904af5204f089beebb7b0301d97704634dd"}, + {file = "pyzmq-25.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:dbc466744a2db4b7ca05589f21ae1a35066afada2f803f92369f5877c100ef62"}, + {file = "pyzmq-25.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:3bed53f7218490c68f0e82a29c92335daa9606216e51c64f37b48eb78f1281f4"}, + {file = "pyzmq-25.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eb52e826d16c09ef87132c6e360e1879c984f19a4f62d8a935345deac43f3c12"}, + {file = "pyzmq-25.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ddbef8b53cd16467fdbfa92a712eae46dd066aa19780681a2ce266e88fbc7165"}, + {file = "pyzmq-25.1.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9301cf1d7fc1ddf668d0abbe3e227fc9ab15bc036a31c247276012abb921b5ff"}, + {file = "pyzmq-25.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e23a8c3b6c06de40bdb9e06288180d630b562db8ac199e8cc535af81f90e64b"}, + {file = "pyzmq-25.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4a82faae00d1eed4809c2f18b37f15ce39a10a1c58fe48b60ad02875d6e13d80"}, + {file = "pyzmq-25.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c8398a1b1951aaa330269c35335ae69744be166e67e0ebd9869bdc09426f3871"}, + {file = "pyzmq-25.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d40682ac60b2a613d36d8d3a0cd14fbdf8e7e0618fbb40aa9fa7b796c9081584"}, + {file = "pyzmq-25.1.0-cp38-cp38-win32.whl", hash = "sha256:33d5c8391a34d56224bccf74f458d82fc6e24b3213fc68165c98b708c7a69325"}, + {file = "pyzmq-25.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c66b7ff2527e18554030319b1376d81560ca0742c6e0b17ff1ee96624a5f1afd"}, + {file = "pyzmq-25.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:af56229ea6527a849ac9fb154a059d7e32e77a8cba27e3e62a1e38d8808cb1a5"}, + {file = "pyzmq-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bdca18b94c404af6ae5533cd1bc310c4931f7ac97c148bbfd2cd4bdd62b96253"}, + {file = "pyzmq-25.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b6b42f7055bbc562f63f3df3b63e3dd1ebe9727ff0f124c3aa7bcea7b3a00f9"}, + {file = "pyzmq-25.1.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c2fc7aad520a97d64ffc98190fce6b64152bde57a10c704b337082679e74f67"}, + {file = "pyzmq-25.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be86a26415a8b6af02cd8d782e3a9ae3872140a057f1cadf0133de685185c02b"}, + {file = "pyzmq-25.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851fb2fe14036cfc1960d806628b80276af5424db09fe5c91c726890c8e6d943"}, + {file = "pyzmq-25.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2a21fec5c3cea45421a19ccbe6250c82f97af4175bc09de4d6dd78fb0cb4c200"}, + {file = "pyzmq-25.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bad172aba822444b32eae54c2d5ab18cd7dee9814fd5c7ed026603b8cae2d05f"}, + {file = "pyzmq-25.1.0-cp39-cp39-win32.whl", hash = "sha256:4d67609b37204acad3d566bb7391e0ecc25ef8bae22ff72ebe2ad7ffb7847158"}, + {file = "pyzmq-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:71c7b5896e40720d30cd77a81e62b433b981005bbff0cb2f739e0f8d059b5d99"}, + {file = "pyzmq-25.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb27ef9d3bdc0c195b2dc54fcb8720e18b741624686a81942e14c8b67cc61a6"}, + {file = "pyzmq-25.1.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0c4fc2741e0513b5d5a12fe200d6785bbcc621f6f2278893a9ca7bed7f2efb7d"}, + {file = "pyzmq-25.1.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fc34fdd458ff77a2a00e3c86f899911f6f269d393ca5675842a6e92eea565bae"}, + {file = "pyzmq-25.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8751f9c1442624da391bbd92bd4b072def6d7702a9390e4479f45c182392ff78"}, + {file = "pyzmq-25.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6581e886aec3135964a302a0f5eb68f964869b9efd1dbafdebceaaf2934f8a68"}, + {file = "pyzmq-25.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5482f08d2c3c42b920e8771ae8932fbaa0a67dff925fc476996ddd8155a170f3"}, + {file = "pyzmq-25.1.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7fbcafa3ea16d1de1f213c226005fea21ee16ed56134b75b2dede5a2129e62"}, + {file = "pyzmq-25.1.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adecf6d02b1beab8d7c04bc36f22bb0e4c65a35eb0b4750b91693631d4081c70"}, + {file = "pyzmq-25.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d39e42a0aa888122d1beb8ec0d4ddfb6c6b45aecb5ba4013c27e2f28657765"}, + {file = "pyzmq-25.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7018289b402ebf2b2c06992813523de61d4ce17bd514c4339d8f27a6f6809492"}, + {file = "pyzmq-25.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9e68ae9864d260b18f311b68d29134d8776d82e7f5d75ce898b40a88df9db30f"}, + {file = "pyzmq-25.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e21cc00e4debe8f54c3ed7b9fcca540f46eee12762a9fa56feb8512fd9057161"}, + {file = "pyzmq-25.1.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f666ae327a6899ff560d741681fdcdf4506f990595201ed39b44278c471ad98"}, + {file = "pyzmq-25.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5efcc29056dfe95e9c9db0dfbb12b62db9c4ad302f812931b6d21dd04a9119"}, + {file = "pyzmq-25.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:48e5e59e77c1a83162ab3c163fc01cd2eebc5b34560341a67421b09be0891287"}, + {file = "pyzmq-25.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:108c96ebbd573d929740d66e4c3d1bdf31d5cde003b8dc7811a3c8c5b0fc173b"}, + {file = "pyzmq-25.1.0.tar.gz", hash = "sha256:80c41023465d36280e801564a69cbfce8ae85ff79b080e1913f6e90481fb8957"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "readme-renderer" +version = "40.0" +description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +optional = false +python-versions = ">=3.8" +files = [ + {file = "readme_renderer-40.0-py3-none-any.whl", hash = "sha256:e18feb2a1e7706f2865b81ebb460056d93fb29d69daa10b223c00faa7bd9a00a"}, + {file = "readme_renderer-40.0.tar.gz", hash = "sha256:9f77b519d96d03d7d7dce44977ba543090a14397c4f60de5b6eb5b8048110aa4"}, +] + +[package.dependencies] +bleach = ">=2.1.0" +docutils = ">=0.13.1" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + +[[package]] +name = "redis" +version = "4.2.2" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.6" +files = [ + {file = "redis-4.2.2-py3-none-any.whl", hash = "sha256:4e95f4ec5f49e636efcf20061a5a9110c20852f607cfca6865c07aaa8a739ee2"}, + {file = "redis-4.2.2.tar.gz", hash = "sha256:0107dc8e98a4f1d1d4aa00100e044287f77121a1e6d2085545c4b7fa94a7a27f"}, +] + +[package.dependencies] +async-timeout = ">=4.0.2" +deprecated = ">=1.2.3" +packaging = ">=20.4" + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "referencing" +version = "0.30.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"}, + {file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.5.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, + {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rpds-py" +version = "0.9.2" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.9.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:ab6919a09c055c9b092798ce18c6c4adf49d24d4d9e43a92b257e3f2548231e7"}, + {file = "rpds_py-0.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d55777a80f78dd09410bd84ff8c95ee05519f41113b2df90a69622f5540c4f8b"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a216b26e5af0a8e265d4efd65d3bcec5fba6b26909014effe20cd302fd1138fa"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29cd8bfb2d716366a035913ced99188a79b623a3512292963d84d3e06e63b496"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44659b1f326214950a8204a248ca6199535e73a694be8d3e0e869f820767f12f"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:745f5a43fdd7d6d25a53ab1a99979e7f8ea419dfefebcab0a5a1e9095490ee5e"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a987578ac5214f18b99d1f2a3851cba5b09f4a689818a106c23dbad0dfeb760f"}, + {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf4151acb541b6e895354f6ff9ac06995ad9e4175cbc6d30aaed08856558201f"}, + {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:03421628f0dc10a4119d714a17f646e2837126a25ac7a256bdf7c3943400f67f"}, + {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13b602dc3e8dff3063734f02dcf05111e887f301fdda74151a93dbbc249930fe"}, + {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fae5cb554b604b3f9e2c608241b5d8d303e410d7dfb6d397c335f983495ce7f6"}, + {file = "rpds_py-0.9.2-cp310-none-win32.whl", hash = "sha256:47c5f58a8e0c2c920cc7783113df2fc4ff12bf3a411d985012f145e9242a2764"}, + {file = "rpds_py-0.9.2-cp310-none-win_amd64.whl", hash = "sha256:4ea6b73c22d8182dff91155af018b11aac9ff7eca085750455c5990cb1cfae6e"}, + {file = "rpds_py-0.9.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:e564d2238512c5ef5e9d79338ab77f1cbbda6c2d541ad41b2af445fb200385e3"}, + {file = "rpds_py-0.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f411330a6376fb50e5b7a3e66894e4a39e60ca2e17dce258d53768fea06a37bd"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e7521f5af0233e89939ad626b15278c71b69dc1dfccaa7b97bd4cdf96536bb7"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d3335c03100a073883857e91db9f2e0ef8a1cf42dc0369cbb9151c149dbbc1b"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d25b1c1096ef0447355f7293fbe9ad740f7c47ae032c2884113f8e87660d8f6e"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a5d3fbd02efd9cf6a8ffc2f17b53a33542f6b154e88dd7b42ef4a4c0700fdad"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5934e2833afeaf36bd1eadb57256239785f5af0220ed8d21c2896ec4d3a765f"}, + {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:095b460e117685867d45548fbd8598a8d9999227e9061ee7f012d9d264e6048d"}, + {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91378d9f4151adc223d584489591dbb79f78814c0734a7c3bfa9c9e09978121c"}, + {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24a81c177379300220e907e9b864107614b144f6c2a15ed5c3450e19cf536fae"}, + {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de0b6eceb46141984671802d412568d22c6bacc9b230174f9e55fc72ef4f57de"}, + {file = "rpds_py-0.9.2-cp311-none-win32.whl", hash = "sha256:700375326ed641f3d9d32060a91513ad668bcb7e2cffb18415c399acb25de2ab"}, + {file = "rpds_py-0.9.2-cp311-none-win_amd64.whl", hash = "sha256:0766babfcf941db8607bdaf82569ec38107dbb03c7f0b72604a0b346b6eb3298"}, + {file = "rpds_py-0.9.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1440c291db3f98a914e1afd9d6541e8fc60b4c3aab1a9008d03da4651e67386"}, + {file = "rpds_py-0.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f2996fbac8e0b77fd67102becb9229986396e051f33dbceada3debaacc7033f"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f30d205755566a25f2ae0382944fcae2f350500ae4df4e795efa9e850821d82"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:159fba751a1e6b1c69244e23ba6c28f879a8758a3e992ed056d86d74a194a0f3"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1f044792e1adcea82468a72310c66a7f08728d72a244730d14880cd1dabe36b"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9251eb8aa82e6cf88510530b29eef4fac825a2b709baf5b94a6094894f252387"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01899794b654e616c8625b194ddd1e5b51ef5b60ed61baa7a2d9c2ad7b2a4238"}, + {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0c43f8ae8f6be1d605b0465671124aa8d6a0e40f1fb81dcea28b7e3d87ca1e1"}, + {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207f57c402d1f8712618f737356e4b6f35253b6d20a324d9a47cb9f38ee43a6b"}, + {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b52e7c5ae35b00566d244ffefba0f46bb6bec749a50412acf42b1c3f402e2c90"}, + {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:978fa96dbb005d599ec4fd9ed301b1cc45f1a8f7982d4793faf20b404b56677d"}, + {file = "rpds_py-0.9.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6aa8326a4a608e1c28da191edd7c924dff445251b94653988efb059b16577a4d"}, + {file = "rpds_py-0.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aad51239bee6bff6823bbbdc8ad85136c6125542bbc609e035ab98ca1e32a192"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd4dc3602370679c2dfb818d9c97b1137d4dd412230cfecd3c66a1bf388a196"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd9da77c6ec1f258387957b754f0df60766ac23ed698b61941ba9acccd3284d1"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:190ca6f55042ea4649ed19c9093a9be9d63cd8a97880106747d7147f88a49d18"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:876bf9ed62323bc7dcfc261dbc5572c996ef26fe6406b0ff985cbcf460fc8a4c"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa2818759aba55df50592ecbc95ebcdc99917fa7b55cc6796235b04193eb3c55"}, + {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ea4d00850ef1e917815e59b078ecb338f6a8efda23369677c54a5825dbebb55"}, + {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5855c85eb8b8a968a74dc7fb014c9166a05e7e7a8377fb91d78512900aadd13d"}, + {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:14c408e9d1a80dcb45c05a5149e5961aadb912fff42ca1dd9b68c0044904eb32"}, + {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:65a0583c43d9f22cb2130c7b110e695fff834fd5e832a776a107197e59a1898e"}, + {file = "rpds_py-0.9.2-cp38-none-win32.whl", hash = "sha256:71f2f7715935a61fa3e4ae91d91b67e571aeb5cb5d10331ab681256bda2ad920"}, + {file = "rpds_py-0.9.2-cp38-none-win_amd64.whl", hash = "sha256:674c704605092e3ebbbd13687b09c9f78c362a4bc710343efe37a91457123044"}, + {file = "rpds_py-0.9.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:07e2c54bef6838fa44c48dfbc8234e8e2466d851124b551fc4e07a1cfeb37260"}, + {file = "rpds_py-0.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fdf55283ad38c33e35e2855565361f4bf0abd02470b8ab28d499c663bc5d7c"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:890ba852c16ace6ed9f90e8670f2c1c178d96510a21b06d2fa12d8783a905193"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50025635ba8b629a86d9d5474e650da304cb46bbb4d18690532dd79341467846"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517cbf6e67ae3623c5127206489d69eb2bdb27239a3c3cc559350ef52a3bbf0b"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0836d71ca19071090d524739420a61580f3f894618d10b666cf3d9a1688355b1"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c439fd54b2b9053717cca3de9583be6584b384d88d045f97d409f0ca867d80f"}, + {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f68996a3b3dc9335037f82754f9cdbe3a95db42bde571d8c3be26cc6245f2324"}, + {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7d68dc8acded354c972116f59b5eb2e5864432948e098c19fe6994926d8e15c3"}, + {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f963c6b1218b96db85fc37a9f0851eaf8b9040aa46dec112611697a7023da535"}, + {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a46859d7f947061b4010e554ccd1791467d1b1759f2dc2ec9055fa239f1bc26"}, + {file = "rpds_py-0.9.2-cp39-none-win32.whl", hash = "sha256:e07e5dbf8a83c66783a9fe2d4566968ea8c161199680e8ad38d53e075df5f0d0"}, + {file = "rpds_py-0.9.2-cp39-none-win_amd64.whl", hash = "sha256:682726178138ea45a0766907957b60f3a1bf3acdf212436be9733f28b6c5af3c"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:196cb208825a8b9c8fc360dc0f87993b8b260038615230242bf18ec84447c08d"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c7671d45530fcb6d5e22fd40c97e1e1e01965fc298cbda523bb640f3d923b387"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83b32f0940adec65099f3b1c215ef7f1d025d13ff947975a055989cb7fd019a4"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f67da97f5b9eac838b6980fc6da268622e91f8960e083a34533ca710bec8611"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03975db5f103997904c37e804e5f340c8fdabbb5883f26ee50a255d664eed58c"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:987b06d1cdb28f88a42e4fb8a87f094e43f3c435ed8e486533aea0bf2e53d931"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c861a7e4aef15ff91233751619ce3a3d2b9e5877e0fcd76f9ea4f6847183aa16"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02938432352359805b6da099c9c95c8a0547fe4b274ce8f1a91677401bb9a45f"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef1f08f2a924837e112cba2953e15aacfccbbfcd773b4b9b4723f8f2ddded08e"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:35da5cc5cb37c04c4ee03128ad59b8c3941a1e5cd398d78c37f716f32a9b7f67"}, + {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:141acb9d4ccc04e704e5992d35472f78c35af047fa0cfae2923835d153f091be"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79f594919d2c1a0cc17d1988a6adaf9a2f000d2e1048f71f298b056b1018e872"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a06418fe1155e72e16dddc68bb3780ae44cebb2912fbd8bb6ff9161de56e1798"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2eb034c94b0b96d5eddb290b7b5198460e2d5d0c421751713953a9c4e47d10"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b08605d248b974eb02f40bdcd1a35d3924c83a2a5e8f5d0fa5af852c4d960af"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0805911caedfe2736935250be5008b261f10a729a303f676d3d5fea6900c96a"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab2299e3f92aa5417d5e16bb45bb4586171c1327568f638e8453c9f8d9e0f020"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c8d7594e38cf98d8a7df25b440f684b510cf4627fe038c297a87496d10a174f"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b9ec12ad5f0a4625db34db7e0005be2632c1013b253a4a60e8302ad4d462afd"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1fcdee18fea97238ed17ab6478c66b2095e4ae7177e35fb71fbe561a27adf620"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:933a7d5cd4b84f959aedeb84f2030f0a01d63ae6cf256629af3081cf3e3426e8"}, + {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:686ba516e02db6d6f8c279d1641f7067ebb5dc58b1d0536c4aaebb7bf01cdc5d"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0173c0444bec0a3d7d848eaeca2d8bd32a1b43f3d3fde6617aac3731fa4be05f"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d576c3ef8c7b2d560e301eb33891d1944d965a4d7a2eacb6332eee8a71827db6"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed89861ee8c8c47d6beb742a602f912b1bb64f598b1e2f3d758948721d44d468"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1054a08e818f8e18910f1bee731583fe8f899b0a0a5044c6e680ceea34f93876"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e7c4bb27ff1aab90dcc3e9d37ee5af0231ed98d99cb6f5250de28889a3d502"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c545d9d14d47be716495076b659db179206e3fd997769bc01e2d550eeb685596"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9039a11bca3c41be5a58282ed81ae422fa680409022b996032a43badef2a3752"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb39aca7a64ad0c9490adfa719dbeeb87d13be137ca189d2564e596f8ba32c07"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2d8b3b3a2ce0eaa00c5bbbb60b6713e94e7e0becab7b3db6c5c77f979e8ed1f1"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:99b1c16f732b3a9971406fbfe18468592c5a3529585a45a35adbc1389a529a03"}, + {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c27ee01a6c3223025f4badd533bea5e87c988cb0ba2811b690395dfe16088cfe"}, + {file = "rpds_py-0.9.2.tar.gz", hash = "sha256:8d70e8f14900f2657c249ea4def963bed86a29b81f81f5b76b5a9215680de945"}, +] + +[[package]] +name = "s3transfer" +version = "0.6.1" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, + {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, +] + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "shellingham" +version = "1.5.0.post1" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"}, + {file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "snowflake-connector-python" +version = "3.0.3" +description = "Snowflake Connector for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "snowflake-connector-python-3.0.3.tar.gz", hash = "sha256:5da1f24edfff63e8b5f27720117c058714b6dc8d26c1c4de4d0d0c55188db966"}, + {file = "snowflake_connector_python-3.0.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:feefde16658b8e5a86536bd7a92b26414664b1fada11c9e4f09ba348b6cc9d14"}, + {file = "snowflake_connector_python-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b390deb323a6300c881740a82a6cea6dd8b6a4d3eba4735b429c3d1df7460cb"}, + {file = "snowflake_connector_python-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:400b0ca919b810df9f0a60d82f77643562625c9fc784e2e539e6af5c999ed2b5"}, + {file = "snowflake_connector_python-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49ec09762e1d418fdeffa1c607237538b09dcd99fec148ab999549055a798438"}, + {file = "snowflake_connector_python-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:a18dc9661abe4ea513c0b3375dff0995726b2ff89e1a6ac5994e8ac8baa11e6a"}, + {file = "snowflake_connector_python-3.0.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1507d6f8c774e2a0a4554f2c99d43316a2eef73943e271638d8cdad0b38e805a"}, + {file = "snowflake_connector_python-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bace1c760f5129b6b0636f0cf1630db0e5f27cc75405768d33d1c2d5ff772a80"}, + {file = "snowflake_connector_python-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c627b81c8602f3a1280bb0cc8ee6c70dab8cbe629336e793fc3bf761e193a4b"}, + {file = "snowflake_connector_python-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee523963fd77aeb7223c9d09df19923b517970c5432279bbe564e5c815ec8613"}, + {file = "snowflake_connector_python-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:4b13d704c2752c13955875b4132ef63499f1b4e13f65357ee642e4dbf17bb718"}, + {file = "snowflake_connector_python-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:090c28319c975c7af13e6932133475401064a118a1c09fc1fb2bd8a9e10e0f56"}, + {file = "snowflake_connector_python-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3556cc1a838b0a6fd5abaf8a9550cea7f78c0203b6197202cd0b94920836c64"}, + {file = "snowflake_connector_python-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:588981518c385bc755fefb5c804b66c68d929e54e366820c11a4c80a1b50dc08"}, + {file = "snowflake_connector_python-3.0.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:50828eeae7e7cb6ce32c6aecdd29bb6cb89ca3e0990f0d4fc97d28b06d290730"}, + {file = "snowflake_connector_python-3.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39f5751f181b6fb8e2cfd48cbe7ac780d55c436a7d91f2ab2240a02362f81090"}, + {file = "snowflake_connector_python-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6b99a63221c8f662cb7b257a607d624d6d7b3393186eba0ce34b1a4303526d7"}, + {file = "snowflake_connector_python-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859571c633b74b46a7d40ac2f24005a874621bc229916cc2229042e59b34b292"}, + {file = "snowflake_connector_python-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:a8eb91f649410884c6f34a8a2514ef6a688f69adf0e287ecf4133477220f42c3"}, + {file = "snowflake_connector_python-3.0.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2409ccebc9a02fb3c38502d937fc6a63d8f3f3f49a79de60a96977463e637259"}, + {file = "snowflake_connector_python-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dbab583afb1958d765d01c1f453d9358ecae5c119c8dbf3777dbfb86e2b3dd4c"}, + {file = "snowflake_connector_python-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1236e8ece702b8a0e739c08a6c2bfbbdc4845df48f75ee6e330d6e75b0eb050"}, + {file = "snowflake_connector_python-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1c4746a5c2d7fc6c5f3a30e06c697137d4a4e878d65d28c748ee3a0db52ef8a"}, + {file = "snowflake_connector_python-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:51ef9e7deadeb0058d32bb4c8ec9e5b063678c62a0b8cb9d3057ec6b4dad80d7"}, +] + +[package.dependencies] +asn1crypto = ">0.24.0,<2.0.0" +certifi = ">=2017.4.17" +cffi = ">=1.9,<2.0.0" +charset-normalizer = ">=2,<3" +cryptography = ">=3.1.0,<41.0.0" +filelock = ">=3.5,<4" +idna = ">=2.5,<4" +oscrypto = "<2.0.0" +packaging = "*" +pandas = {version = ">=1.0.0,<1.6.0", optional = true, markers = "extra == \"pandas\""} +pyarrow = {version = ">=10.0.1,<10.1.0", optional = true, markers = "extra == \"pandas\""} +pycryptodomex = ">=3.2,<3.5.0 || >3.5.0,<4.0.0" +pyjwt = "<3.0.0" +pyOpenSSL = ">=16.2.0,<24.0.0" +pytz = "*" +requests = "<3.0.0" +typing-extensions = ">=4.3,<5" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +development = ["Cython", "coverage", "more-itertools", "numpy (<1.25.0)", "pendulum (!=2.1.1)", "pexpect", "pytest (<7.3.0)", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist", "pytzdata"] +pandas = ["pandas (>=1.0.0,<1.6.0)", "pyarrow (>=10.0.1,<10.1.0)"] +secure-local-storage = ["keyring (!=16.1.0,<24.0.0)"] + +[[package]] +name = "sqlalchemy" +version = "1.4.49" +description = "Database Abstraction Library" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "SQLAlchemy-1.4.49-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6"}, + {file = "SQLAlchemy-1.4.49-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"}, + {file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} +mypy = {version = ">=0.910", optional = true, markers = "python_version >= \"3\" and extra == \"mypy\""} +sqlalchemy2-stubs = {version = "*", optional = true, markers = "extra == \"mypy\""} + +[package.extras] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql", "pymysql (<1)"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a35" +description = "Typing Stubs for SQLAlchemy 1.4" +optional = false +python-versions = ">=3.6" +files = [ + {file = "sqlalchemy2-stubs-0.0.2a35.tar.gz", hash = "sha256:bd5d530697d7e8c8504c7fe792ef334538392a5fb7aa7e4f670bfacdd668a19d"}, + {file = "sqlalchemy2_stubs-0.0.2a35-py3-none-any.whl", hash = "sha256:593784ff9fc0dc2ded1895e3322591689db3be06f3ca006e3ef47640baf2d38a"}, +] + +[package.dependencies] +typing-extensions = ">=3.7.4" + +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "starlette" +version = "0.27.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.7" +files = [ + {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, + {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tenacity" +version = "8.2.2" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.6" +files = [ + {file = "tenacity-8.2.2-py3-none-any.whl", hash = "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0"}, + {file = "tenacity-8.2.2.tar.gz", hash = "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "toolz" +version = "0.12.0" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.5" +files = [ + {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"}, + {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"}, +] + +[[package]] +name = "tornado" +version = "6.3.2" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829"}, + {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c"}, + {file = "tornado-6.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f"}, + {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4"}, + {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe"}, + {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d"}, + {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0"}, + {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411"}, + {file = "tornado-6.3.2-cp38-abi3-win32.whl", hash = "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2"}, + {file = "tornado-6.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf"}, + {file = "tornado-6.3.2.tar.gz", hash = "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba"}, +] + +[[package]] +name = "tqdm" +version = "4.65.0" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, + {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "traitlets" +version = "5.9.0" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.7" +files = [ + {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, + {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] + +[[package]] +name = "twine" +version = "4.0.2" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.7" +files = [ + {file = "twine-4.0.2-py3-none-any.whl", hash = "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8"}, + {file = "twine-4.0.2.tar.gz", hash = "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8"}, +] + +[package.dependencies] +importlib-metadata = ">=3.6" +keyring = ">=15.1" +pkginfo = ">=1.8.1" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[[package]] +name = "typeguard" +version = "2.13.3" +description = "Run-time type checker for Python" +optional = false +python-versions = ">=3.5.3" +files = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] + +[package.extras] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy", "pytest", "typing-extensions"] + +[[package]] +name = "typer" +version = "0.9.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.6" +files = [ + {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, + {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} +rich = {version = ">=10.11.0,<14.0.0", optional = true, markers = "extra == \"all\""} +shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + +[[package]] +name = "types-awscrt" +version = "0.17.0" +description = "Type annotations and code completion for awscrt" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "types_awscrt-0.17.0-py3-none-any.whl", hash = "sha256:278c1a913098e226769cd28dc37643a2f360907353a7e8918e1bcdf93d4e34f2"}, + {file = "types_awscrt-0.17.0.tar.gz", hash = "sha256:4214783a747af900a5f98ec020d52ecae5910b470fd636813637a45b82a97516"}, +] + +[[package]] +name = "types-boto3" +version = "1.0.2" +description = "Proxy package for boto3-stubs" +optional = false +python-versions = "*" +files = [ + {file = "types-boto3-1.0.2.tar.gz", hash = "sha256:15f3ffad0314e40a0708fec25f94891414f93260202422bf8b19b6913853c983"}, + {file = "types_boto3-1.0.2-py3-none-any.whl", hash = "sha256:a6a88e94d59d887839863a64095493956efc148e747206880a7eb47d90ae8398"}, +] + +[package.dependencies] +boto3-stubs = "*" + +[[package]] +name = "types-pyyaml" +version = "6.0.12.11" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, + {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, +] + +[[package]] +name = "types-s3transfer" +version = "0.6.1" +description = "Type annotations and code completion for s3transfer" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "types_s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:6d1ac1dedac750d570428362acdf60fdd4f277b0788855c3894d3226756b2bfb"}, + {file = "types_s3transfer-0.6.1.tar.gz", hash = "sha256:75ac1d7143d58c1e6af467cfd4a96c67ee058a3adf7c249d9309999e1f5f41e4"}, +] + +[package.dependencies] +types-awscrt = "*" + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "uvicorn" +version = "0.22.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.7" +files = [ + {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"}, + {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.17.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"}, + {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"}, + {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"}, + {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"}, + {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"}, + {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"}, + {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"}, + {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"}, + {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"}, + {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"}, + {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"}, + {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"}, + {file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"}, + {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"}, + {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"}, + {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"}, + {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"}, + {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"}, + {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"}, + {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"}, + {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"}, + {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"}, + {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"}, + {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"}, + {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"}, + {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"}, + {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"}, + {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"}, + {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"}, + {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, +] + +[package.extras] +dev = ["Cython (>=0.29.32,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] + +[[package]] +name = "volatile" +version = "2.1.0" +description = "A small extension for the tempfile module." +optional = false +python-versions = "*" +files = [ + {file = "volatile-2.1.0.tar.gz", hash = "sha256:9be36ad508e3354e016c115de0397dc2203b9800a73d9d177ca9d37a8d3a31d3"}, +] + +[[package]] +name = "watchfiles" +version = "0.19.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "watchfiles-0.19.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:91633e64712df3051ca454ca7d1b976baf842d7a3640b87622b323c55f3345e7"}, + {file = "watchfiles-0.19.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b6577b8c6c8701ba8642ea9335a129836347894b666dd1ec2226830e263909d3"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:18b28f6ad871b82df9542ff958d0c86bb0d8310bb09eb8e87d97318a3b5273af"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac19dc9cbc34052394dbe81e149411a62e71999c0a19e1e09ce537867f95ae0"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ea3397aecbc81c19ed7f025e051a7387feefdb789cf768ff994c1228182fda"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0376deac92377817e4fb8f347bf559b7d44ff556d9bc6f6208dd3f79f104aaf"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c75eff897786ee262c9f17a48886f4e98e6cfd335e011c591c305e5d083c056"}, + {file = "watchfiles-0.19.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb5d45c4143c1dd60f98a16187fd123eda7248f84ef22244818c18d531a249d1"}, + {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:79c533ff593db861ae23436541f481ec896ee3da4e5db8962429b441bbaae16e"}, + {file = "watchfiles-0.19.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3d7d267d27aceeeaa3de0dd161a0d64f0a282264d592e335fff7958cc0cbae7c"}, + {file = "watchfiles-0.19.0-cp37-abi3-win32.whl", hash = "sha256:176a9a7641ec2c97b24455135d58012a5be5c6217fc4d5fef0b2b9f75dbf5154"}, + {file = "watchfiles-0.19.0-cp37-abi3-win_amd64.whl", hash = "sha256:945be0baa3e2440151eb3718fd8846751e8b51d8de7b884c90b17d271d34cae8"}, + {file = "watchfiles-0.19.0-cp37-abi3-win_arm64.whl", hash = "sha256:0089c6dc24d436b373c3c57657bf4f9a453b13767150d17284fc6162b2791911"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cae3dde0b4b2078f31527acff6f486e23abed307ba4d3932466ba7cdd5ecec79"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f3920b1285a7d3ce898e303d84791b7bf40d57b7695ad549dc04e6a44c9f120"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9afd0d69429172c796164fd7fe8e821ade9be983f51c659a38da3faaaaac44dc"}, + {file = "watchfiles-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68dce92b29575dda0f8d30c11742a8e2b9b8ec768ae414b54f7453f27bdf9545"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:5569fc7f967429d4bc87e355cdfdcee6aabe4b620801e2cf5805ea245c06097c"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5471582658ea56fca122c0f0d0116a36807c63fefd6fdc92c71ca9a4491b6b48"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b538014a87f94d92f98f34d3e6d2635478e6be6423a9ea53e4dd96210065e193"}, + {file = "watchfiles-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b44221764955b1e703f012c74015306fb7e79a00c15370785f309b1ed9aa8d"}, + {file = "watchfiles-0.19.0.tar.gz", hash = "sha256:d9b073073e048081e502b6c6b0b88714c026a1a4c890569238d04aca5f9ca74b"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websockets" +version = "11.0.3" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, + {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, + {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, + {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, + {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, + {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, + {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, + {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, + {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, + {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, + {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, + {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, + {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, + {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, +] + +[[package]] +name = "wheel" +version = "0.41.1" +description = "A built-package format for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "wheel-0.41.1-py3-none-any.whl", hash = "sha256:473219bd4cbedc62cea0cb309089b593e47c15c4a2531015f94e4e3b9a0f6981"}, + {file = "wheel-0.41.1.tar.gz", hash = "sha256:12b911f083e876e10c595779709f8a88a59f45aacc646492a67fe9ef796c1b47"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + +[[package]] +name = "wrapt" +version = "1.15.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +] + +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + +[[package]] +name = "zipp" +version = "3.16.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, + {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8,<3.11" +content-hash = "87147131645c390664d3a03b6eeeee9197ad022ec6f6fb6d115eeb4f3cd12383" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a4ed7a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,76 @@ +[tool.poetry] +name = "wyvern-ai" +version = "0.0.1" +description = "" +authors = ["Wyvern AI "] +readme = "README.md" +packages = [ + { include = "wyvern" }, +] + +[tool.poetry.dependencies] +python = ">=3.8,<3.11" +pydantic = "^1.10.4" +fastapi = "^0.95.2" +uvicorn = "^0.22.0" +typer = {extras = ["all"], version = "^0.9.0"} +pyyaml = "^6.0" +pyhumps = "^3.8.0" +python-dotenv = "^1.0.0" +pandas = "1.5.3" +feast = {extras = ["redis", "snowflake"], version = "^0.31.1"} +httpx = "^0.24.1" +snowflake-connector-python = "3.0.3" +boto3 = "^1.26.146" +ddtrace = "^1.14.0" +msgspec = "^0.16.0" +lz4 = "^4.3.2" +more-itertools = "^9.1.0" +tqdm = "^4.65.0" +nest-asyncio = "^1.5.7" + + +[tool.poetry.group.dev.dependencies] +ipython = "^8.9.0" +pytest = "^7.2.1" +isort = "^5.12.0" +types-pyyaml = "^6.0.12.6" +black = "^22.6.0" +pip-tools = "^6.12.2" +twine = "^4.0.2" +pytest-asyncio = "^0.21.0" +pytest-mock = "^3.10.0" +types-boto3 = "^1.0.2" +pyinstrument = "^4.4.0" +pytest-dotenv = "^0.5.2" +ipykernel = "^6.25.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[[tool.mypy.overrides]] +module=[ + "sentence_transformers.*", + "scipy.spatial.distance.*", + "featureform.*", + "setuptools.*", + "ddtrace.*", + "nest_asyncio.*", + "lz4.*", +] +ignore_missing_imports = true + +[tool.isort] +profile = "black" + +[tool.pytest.ini_options] +addopts = "-v" +filterwarnings = ["ignore::DeprecationWarning"] +log_cli = true +log_cli_level = "INFO" +log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" +log_cli_date_format = "%Y-%m-%d %H:%M:%S" +env_files = [ + ".testing_env", +] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2b50fef --- /dev/null +++ b/setup.cfg @@ -0,0 +1,9 @@ +[flake8] +max-line-length = 120 +# black will introduce W503 which is not PEP8 compatible right now. However, https://peps.python.org/pep-0008/#should-a-line-break-before-or-after-a-binary-operator +# Ignore E203 due to https://github.com/psf/black/issues/315 +ignore = N805,N802,B008,W503,E203 +extend-immutable-calls = fastapi.Depends, fastapi.params.Depends + +[metadata] +description-file = README.md diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/components/__init__.py b/tests/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/components/business_logic/__init__.py b/tests/components/business_logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/components/business_logic/test_pinning_business_logic.py b/tests/components/business_logic/test_pinning_business_logic.py new file mode 100644 index 0000000..f0b945f --- /dev/null +++ b/tests/components/business_logic/test_pinning_business_logic.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +from typing import Dict, List + +import pytest + +from wyvern import request_context +from wyvern.components.business_logic.business_logic import ( + BusinessLogicPipeline, + BusinessLogicRequest, + BusinessLogicResponse, +) +from wyvern.components.business_logic.pinning_business_logic import ( + PinningBusinessLogicComponent, +) +from wyvern.entities.candidate_entities import ScoredCandidate +from wyvern.entities.feature_entities import FeatureMap +from wyvern.entities.identifier_entities import ProductEntity +from wyvern.entities.request import BaseWyvernRequest +from wyvern.wyvern_request import WyvernRequest + + +async def set_up_pinning_components( + scored_candidates: List[ScoredCandidate[ProductEntity]], + entity_pins: Dict[str, int], + allow_down_ranking=True, +) -> BusinessLogicResponse[ProductEntity, BaseWyvernRequest]: + class TestPins(PinningBusinessLogicComponent[ProductEntity, BaseWyvernRequest]): + async def execute( + self, + input: BusinessLogicRequest[ProductEntity, BaseWyvernRequest], + **kwargs, + ) -> List[ScoredCandidate[ProductEntity]]: + return self.pin( + input.scored_candidates, + entity_pins=entity_pins, + allow_down_ranking=allow_down_ranking, + ) + + class TestBusinessLogicPipeline( + BusinessLogicPipeline[ProductEntity, BaseWyvernRequest], + ): + def __init__(self): + """ + Add new business logic components here. All business logic steps are executed in the order defined here. + """ + super().__init__( + TestPins(), + name="test_business_logic_pipeline", + ) + + pipeline = TestBusinessLogicPipeline() + await pipeline.initialize() + + request = BusinessLogicRequest[ProductEntity, BaseWyvernRequest]( + request=BaseWyvernRequest(request_id="123"), + scored_candidates=scored_candidates, + ) + + request_context.set( + WyvernRequest( + method="POST", + url="TestTest", + url_path="Test", + json=request, + headers={}, + entity_store={}, + events=[], + feature_map=FeatureMap(feature_map={}), + ), + ) + return await pipeline.execute(request) + + +def generate_scored_candidates(id_score_pairs: Dict[str, float]): + return [ + ScoredCandidate(entity=ProductEntity(product_id=id), score=score) + for id, score in id_score_pairs.items() + ] + + +@pytest.mark.asyncio +async def test_pins(): + scored_candidates = generate_scored_candidates( + { + "product_1": 6, + "product_2": 5, + "product_3": 4, + "product_4": 3, + "product_5": 2, + "product_6": 1, + }, + ) + + pins = { + "product_6": 11, + "product_5": 10, + "product_3": 0, + "product_2": 0, + "product_4": 2, + } + + result = await set_up_pinning_components(scored_candidates, pins) + + adjusted_candidates = [ + candidate.entity.product_id for candidate in result.adjusted_candidates + ] + # Bug -- product_4 is coming in at index 3, not index 2 like requested.. due to the other boosts + expected_order = [ + "product_2", + "product_3", + "product_1", + "product_4", + "product_5", + "product_6", + ] + assert adjusted_candidates == expected_order + + +@pytest.mark.asyncio +async def test_pins__no_down_ranking(): + scored_candidates = generate_scored_candidates( + { + "product_1": 6, + "product_2": 5, + "product_3": 4, + "product_4": 3, + "product_5": 2, + "product_6": 1, + }, + ) + + pins = { + "product_6": 11, + "product_5": 12, + "product_3": 0, + "product_2": 22, + "product_4": 2, + } + + result = await set_up_pinning_components( + scored_candidates, + pins, + allow_down_ranking=False, + ) + + adjusted_candidates = [ + candidate.entity.product_id for candidate in result.adjusted_candidates + ] + # Bug -- product_4 is coming in at index 3, not index 2 like requested.. due to the other boosts + expected_order = [ + "product_3", + "product_1", + "product_2", + "product_4", + "product_5", + "product_6", + ] + assert adjusted_candidates == expected_order + + +""" +TODO (suchintan): +Test cases: +1. Pin any product +2. Pin multiple products in different order +3. Allow down ranking = false and true +4. Pin a product that is not in the list +5. Pin a product that is in the list but not in the top 10 +6. Pin multiple products to the same position +7. Pin to the top of the list +8. Pin to the bottom of the list +""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2ca0b9d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +import pytest +from ddtrace import tracer + + +@pytest.fixture(scope="session", autouse=True) +def disable_ddtrace(): + tracer.enabled = False diff --git a/tests/feature_store/__init__.py b/tests/feature_store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/feature_store/test_real_time_features.py b/tests/feature_store/test_real_time_features.py new file mode 100644 index 0000000..deb24d4 --- /dev/null +++ b/tests/feature_store/test_real_time_features.py @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- +import pytest +from starlette.testclient import TestClient + +from examples.real_time_features_main import * # noqa: F401, F403 +from examples.real_time_features_main import RealTimeFeatureTestingComponent +from tests.scenarios.test_product_ranking import ( # noqa: F401 + RankingRealtimeFeatureComponent, +) +from wyvern.components.features.feature_store import feature_store_retrieval_component +from wyvern.entities.feature_entities import FeatureMap +from wyvern.feature_store.historical_feature_util import separate_real_time_features +from wyvern.service import WyvernService + + +@pytest.fixture +def mock_redis(mocker): + """ + Mocks the redis call. Each entry under `return_value` corresponds to a single entity fetch from Redis + """ + with mocker.patch( + "wyvern.redis.wyvern_redis.mget", + return_value=[None, None, None, None, None], + ): + yield + + +@pytest.fixture +def mock_feature_store(mocker): + with mocker.patch.object( + feature_store_retrieval_component, + "fetch_features_from_feature_store", + return_value=FeatureMap(feature_map={}), + ): + yield + + +@pytest.fixture +def test_client(): + wyvern_service = WyvernService.generate( + route_components=[RealTimeFeatureTestingComponent], + ) + yield TestClient(wyvern_service.service.app) + + +@pytest.mark.asyncio +async def test_end_to_end(mock_redis, test_client, mock_feature_store): + response = test_client.post( + "/api/v1/real-time-features-testing", + json={ + "request_id": "test_request_id", + "query": {"query": "candle"}, + "candidates": [ + { + "product_id": "p1", + "opensearch_score": 1, + "matched_queries": ["QUERY_1", "QUERY_2"], + }, + {"product_id": "p2"}, + {"product_id": "p3"}, + ], + "user": {"user_id": "1234", "user_name": "user_name"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "feature_data": { + "request::test_request_id": { + "identifier": { + "identifier": "test_request_id", + "identifier_type": "request", + }, + "features": { + "RealTimeNumberOfCandidatesFeature:f_number_of_candidates": 3.0, + }, + }, + "query::candle": { + "identifier": {"identifier": "candle", "identifier_type": "query"}, + "features": { + "RealTimeQueryFeature:f_query_length": 6.0, + "RealTimeStringFeature:f_query": "candle", + "RealTimeEmbeddingFeature:f_query_embedding_vector_8": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + ], + }, + }, + "user::1234": { + "identifier": {"identifier": "1234", "identifier_type": "user"}, + "features": {"RealTimeUserFeature:f_user_name_length": 9.0}, + }, + "query:user::candle:1234": { + "identifier": { + "identifier": "candle:1234", + "identifier_type": "query:user", + }, + "features": { + "RealTimeUserQueryFeature:f_user_query_name_edit_distance": 3.0, + "RealTimeUserQueryFeature:f_user_query_name_jaccard_similarity": -3.0, + }, + }, + "product::p1": { + "identifier": {"identifier": "p1", "identifier_type": "product"}, + "features": {"RealTimeProductFeature:f_opensearch_score": 1.0}, + }, + "product::p2": { + "identifier": {"identifier": "p2", "identifier_type": "product"}, + "features": {}, + }, + "product::p3": { + "identifier": {"identifier": "p3", "identifier_type": "product"}, + "features": {}, + }, + "product:query::p1:candle": { + "identifier": { + "identifier": "p1:candle", + "identifier_type": "product:query", + }, + "features": { + "RealTimeMatchedQueriesProductFeature:f_matched_queries_QUERY_1": 1.0, + "RealTimeMatchedQueriesProductFeature:f_matched_queries_QUERY_2": 1.0, + "RealTimeQueryProductFeature:f_query_product_name_edit_distance": 4.0, + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity": -4.0, + }, + }, + "product:query::p2:candle": { + "identifier": { + "identifier": "p2:candle", + "identifier_type": "product:query", + }, + "features": { + "RealTimeQueryProductFeature:f_query_product_name_edit_distance": 4.0, + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity": -4.0, + }, + }, + "product:query::p3:candle": { + "identifier": { + "identifier": "p3:candle", + "identifier_type": "product:query", + }, + "features": { + "RealTimeQueryProductFeature:f_query_product_name_edit_distance": 4.0, + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity": -4.0, + }, + }, + "product:user::p1:1234": { + "identifier": { + "identifier": "p1:1234", + "identifier_type": "product:user", + }, + "features": { + "RealTimeUserProductFeature:f_user_product_name_edit_distance": 7.0, + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity": -7.0, + }, + }, + "product:user::p2:1234": { + "identifier": { + "identifier": "p2:1234", + "identifier_type": "product:user", + }, + "features": { + "RealTimeUserProductFeature:f_user_product_name_edit_distance": 7.0, + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity": -7.0, + }, + }, + "product:user::p3:1234": { + "identifier": { + "identifier": "p3:1234", + "identifier_type": "product:user", + }, + "features": { + "RealTimeUserProductFeature:f_user_product_name_edit_distance": 7.0, + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity": -7.0, + }, + }, + }, + } + + +@pytest.fixture +def mock_redis__2(mocker): + """ + Mocks the redis call. Each entry under `return_value` corresponds to a single entity fetch from Redis + """ + with mocker.patch( + "wyvern.redis.wyvern_redis.mget", + return_value=[None, None, None, None, None, None], + ): + yield + + +@pytest.mark.asyncio +async def test_end_to_end__2(mock_redis__2, test_client): + response = test_client.post( + "/api/v1/real-time-features-testing", + json={ + "request_id": "test_request_id", + "query": {"query": "candle"}, + "candidates": [ + { + "product_id": "p1", + "opensearch_score": 1, + "matched_queries": ["QUERY_1", "QUERY_2"], + }, + {"product_id": "p2"}, + {"product_id": "p3"}, + { + "product_id": "p4", + "opensearch_score": 100, + "matched_queries": ["MATIAS", "QUERY_2"], + }, + ], + "user": {"user_id": "1234", "user_name": "user_name"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "feature_data": { + "request::test_request_id": { + "identifier": { + "identifier": "test_request_id", + "identifier_type": "request", + }, + "features": { + "RealTimeNumberOfCandidatesFeature:f_number_of_candidates": 4.0, + }, + }, + "query::candle": { + "identifier": {"identifier": "candle", "identifier_type": "query"}, + "features": { + "RealTimeQueryFeature:f_query_length": 6.0, + "RealTimeStringFeature:f_query": "candle", + "RealTimeEmbeddingFeature:f_query_embedding_vector_8": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + ], + }, + }, + "user::1234": { + "identifier": {"identifier": "1234", "identifier_type": "user"}, + "features": {"RealTimeUserFeature:f_user_name_length": 9.0}, + }, + "query:user::candle:1234": { + "identifier": { + "identifier": "candle:1234", + "identifier_type": "query:user", + }, + "features": { + "RealTimeUserQueryFeature:f_user_query_name_edit_distance": 3.0, + "RealTimeUserQueryFeature:f_user_query_name_jaccard_similarity": -3.0, + }, + }, + "product::p1": { + "identifier": {"identifier": "p1", "identifier_type": "product"}, + "features": {"RealTimeProductFeature:f_opensearch_score": 1.0}, + }, + "product::p2": { + "identifier": {"identifier": "p2", "identifier_type": "product"}, + "features": {}, + }, + "product::p3": { + "identifier": {"identifier": "p3", "identifier_type": "product"}, + "features": {}, + }, + "product::p4": { + "identifier": {"identifier": "p4", "identifier_type": "product"}, + "features": {"RealTimeProductFeature:f_opensearch_score": 100.0}, + }, + "product:query::p1:candle": { + "identifier": { + "identifier": "p1:candle", + "identifier_type": "product:query", + }, + "features": { + "RealTimeMatchedQueriesProductFeature:f_matched_queries_QUERY_1": 1.0, + "RealTimeMatchedQueriesProductFeature:f_matched_queries_QUERY_2": 1.0, + "RealTimeQueryProductFeature:f_query_product_name_edit_distance": 4.0, + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity": -4.0, + }, + }, + "product:query::p2:candle": { + "identifier": { + "identifier": "p2:candle", + "identifier_type": "product:query", + }, + "features": { + "RealTimeQueryProductFeature:f_query_product_name_edit_distance": 4.0, + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity": -4.0, + }, + }, + "product:query::p3:candle": { + "identifier": { + "identifier": "p3:candle", + "identifier_type": "product:query", + }, + "features": { + "RealTimeQueryProductFeature:f_query_product_name_edit_distance": 4.0, + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity": -4.0, + }, + }, + "product:query::p4:candle": { + "identifier": { + "identifier": "p4:candle", + "identifier_type": "product:query", + }, + "features": { + "RealTimeMatchedQueriesProductFeature:f_matched_queries_MATIAS": 1.0, + "RealTimeMatchedQueriesProductFeature:f_matched_queries_QUERY_2": 1.0, + "RealTimeQueryProductFeature:f_query_product_name_edit_distance": 4.0, + "RealTimeQueryProductFeature:f_query_product_name_jaccard_similarity": -4.0, + }, + }, + "product:user::p1:1234": { + "identifier": { + "identifier": "p1:1234", + "identifier_type": "product:user", + }, + "features": { + "RealTimeUserProductFeature:f_user_product_name_edit_distance": 7.0, + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity": -7.0, + }, + }, + "product:user::p2:1234": { + "identifier": { + "identifier": "p2:1234", + "identifier_type": "product:user", + }, + "features": { + "RealTimeUserProductFeature:f_user_product_name_edit_distance": 7.0, + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity": -7.0, + }, + }, + "product:user::p3:1234": { + "identifier": { + "identifier": "p3:1234", + "identifier_type": "product:user", + }, + "features": { + "RealTimeUserProductFeature:f_user_product_name_edit_distance": 7.0, + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity": -7.0, + }, + }, + "product:user::p4:1234": { + "identifier": { + "identifier": "p4:1234", + "identifier_type": "product:user", + }, + "features": { + "RealTimeUserProductFeature:f_user_product_name_edit_distance": 7.0, + "RealTimeUserProductFeature:f_user_product_name_jaccard_similarity": -7.0, + }, + }, + }, + } + + +def test_separate_real_time_features(): + realtime_features, non_realtime_features = separate_real_time_features( + [ + "RealTimeProductFeature:f_opensearch_score", + "RankingRealtimeFeatureComponent:f_1", + "RankingRealtimeFeatureComponent:f_3", + "random_fv:fn1", + ], + ) + assert realtime_features == [ + "RealTimeProductFeature:f_opensearch_score", + "RankingRealtimeFeatureComponent:f_1", + "RankingRealtimeFeatureComponent:f_3", + ] + assert non_realtime_features == ["random_fv:fn1"] diff --git a/tests/load_test/load_test.js b/tests/load_test/load_test.js new file mode 100644 index 0000000..bd3fce8 --- /dev/null +++ b/tests/load_test/load_test.js @@ -0,0 +1,58 @@ +// Install k6 `brew install k6` +import http from "k6/http"; +import { sleep, check } from "k6"; + +export const options = { + vus: 1, + duration: "5s", +}; + +export default function () { + const api_key = ""; + + const params = { + headers: { + "Content-Type": "application/json", + "x-api-key": api_key, + }, + }; + + const candidates = Array.from({ length: 1000 }, (_, i) => ({ + product_id: i + Math.floor(Math.random() * 10000), + opensearch_score: 1000 - i, + })); + + const payload = JSON.stringify({ + request_id: "test_request_id", + query: { query: "candle" }, + candidates: candidates, + user: { user_id: "1234", user_name: "user_name" }, + user_page_size: 10, + user_page: 20, + candidate_page_size: 1000, + candidate_page: 0, + }); + + const r = http.post( + "https://api.wyvern.ai/api/v1/product-search-ranking", + payload, + params + ); + check(r, { "status was 200": (r) => r.status == 200 }); + console.log(r.body); + sleep(1); +} +/* +{ + "request_id": "test_request_id", + "query": {"query": "candle"}, + "candidates": [ + {"product_id": "0", "opensearch_score": 1000}, + ], + "user": {"user_id": "1234", "user_name": "user_name"}, + "user_page_size": 10, + "user_page": 20, + "candidate_page_size": 1000, + "candidate_page": 0 +} +*/ diff --git a/tests/scenarios/__init__.py b/tests/scenarios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/scenarios/test_indexation.py b/tests/scenarios/test_indexation.py new file mode 100644 index 0000000..44e3e06 --- /dev/null +++ b/tests/scenarios/test_indexation.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +import pytest +from fastapi.testclient import TestClient + +from wyvern.service import WyvernService + +PRODUCT_ENTITY_1 = { + "product_id": "1", + "product_name": "test_product1", + "product_description": "test_product1_description", +} +PRODUCT_ENTITY_2 = { + "product_id": "2", + "product_name": "test_product2", + "product_description": "test_product2_description", +} + +PRODUCT_ENTITY_1_WITH_ID = { + "id": "1", + "product_name": "test_product1", + "product_description": "test_product1_description", +} +PRODUCT_ENTITY_2_WITH_ID = { + "id": "2", + "product_name": "test_product2", + "product_description": "test_product2_description", +} + + +@pytest.fixture +def mock_redis(mocker): + with mocker.patch( + "wyvern.redis.wyvern_redis.bulk_index", + return_value=["1", "2"], + ), mocker.patch( + "wyvern.redis.wyvern_redis.get_entity", + return_value=PRODUCT_ENTITY_1, + ), mocker.patch( + "wyvern.redis.wyvern_redis.get_entities", + return_value=[ + PRODUCT_ENTITY_1, + PRODUCT_ENTITY_2, + ], + ), mocker.patch( + "wyvern.redis.wyvern_redis.delete_entity", + ), mocker.patch( + "wyvern.redis.wyvern_redis.delete_entities", + ): + yield + + +@pytest.fixture +def test_client(mock_redis): + wyvern_service = WyvernService.generate() + yield TestClient(wyvern_service.service.app) + + +@pytest.mark.asyncio +async def test_product_upload(test_client): + response = test_client.post( + "/api/v1/entities/upload", + json={ + "entities": [ + PRODUCT_ENTITY_1, + PRODUCT_ENTITY_2, + ], + "entity_type": "product", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "entity_type": "product", + "entity_ids": ["1", "2"], + } + + +@pytest.mark.asyncio +async def test_product_upload__with_different_entity_key(test_client): + response = test_client.post( + "/api/v1/entities/upload", + json={ + "entities": [ + PRODUCT_ENTITY_1_WITH_ID, + PRODUCT_ENTITY_2_WITH_ID, + ], + "entity_type": "product", + "entity_key": "id", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "entity_type": "product", + "entity_ids": ["1", "2"], + } + + +@pytest.mark.asyncio +async def test_get_products(test_client): + response = test_client.post( + "/api/v1/entities/get", + json={ + "entity_ids": ["1", "2"], + "entity_type": "product", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "entity_type": "product", + "entities": { + "1": PRODUCT_ENTITY_1, + "2": PRODUCT_ENTITY_2, + }, + } + + +@pytest.mark.asyncio +async def test_delete_products(test_client): + response = test_client.post( + "/api/v1/entities/delete", + json={ + "entity_ids": ["1", "2"], + "entity_type": "product", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "entity_type": "product", + "entity_ids": ["1", "2"], + } diff --git a/tests/scenarios/test_product_ranking.py b/tests/scenarios/test_product_ranking.py new file mode 100644 index 0000000..23d6142 --- /dev/null +++ b/tests/scenarios/test_product_ranking.py @@ -0,0 +1,512 @@ +# -*- coding: utf-8 -*- +import json +from functools import cached_property +from typing import Any, Dict, List, Optional, Set + +import httpx +import pytest +from fastapi.testclient import TestClient +from pydantic import BaseModel + +from wyvern import request_context +from wyvern.components.features.realtime_features_component import ( + RealtimeFeatureComponent, + RealtimeFeatureRequest, +) +from wyvern.components.models.model_component import ( + ModelComponent, + ModelInput, + ModelOutput, +) +from wyvern.components.pipeline_component import PipelineComponent +from wyvern.core.compression import wyvern_encode +from wyvern.entities.candidate_entities import CandidateSetEntity +from wyvern.entities.feature_entities import FeatureData, FeatureMap +from wyvern.entities.identifier import Identifier +from wyvern.entities.identifier_entities import ProductEntity, WyvernEntity +from wyvern.entities.request import BaseWyvernRequest +from wyvern.service import WyvernService +from wyvern.wyvern_request import WyvernRequest + +PRODUCT_ENTITY_1 = { + "product_id": "test_product1", + "product_name": "test_product1 name", + "product_description": "test_product1_description", + "brand_id": "test_brand1", +} +PRODUCT_ENTITY_2 = { + "product_id": "test_product2", + "product_name": "test_product2 name", + "product_description": "test_product2_description", + "brand_id": "test_brand2", +} +PRODUCT_ENTITY_2__DUPLICATE_BRAND = { + "product_id": "test_product2", + "product_name": "test_product2 name", + "product_description": "test_product2_description", + "brand_id": "test_brand1", +} +USER_ENTITY = { + "user_id": "test_user", + "name": "test user name", +} +BRAND_ENTITY_1 = { + "brand_id": "test_brand1", + "name": "test brand1 name", +} +BRAND_ENTITY_2 = { + "brand_id": "test_brand2", + "name": "test brand2 name", +} + +ONLINE_FEATURE_RESPNOSE = { + "metadata": { + "feature_names": [ + "IDENTIFIER", + "test_batch_feature", + ], + }, + "results": [ + { + "values": [ + "test_product1", + "test_brand1", + "test_product2", + "test_brand2", + "test query", + "test_user", + ], + "statuses": ["PRESENT", "PRESENT"], + "event_timestamps": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"], + }, + { + "values": [ + None, + None, + None, + None, + None, + None, + ], + "statuses": ["PRESENT", "PRESENT"], + "event_timestamps": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"], + }, + ], +} + + +@pytest.fixture +def mock_httpx_post(mocker): + mocked_httpx_async_client = httpx.AsyncClient() + with mocker.patch.object( + mocked_httpx_async_client, + "post", + return_value=httpx.Response( + status_code=200, + content=json.dumps(ONLINE_FEATURE_RESPNOSE), + headers={}, + json=ONLINE_FEATURE_RESPNOSE, + ), + ): + with mocker.patch( + "wyvern.components.features.feature_store.httpx_client", + return_value=mocked_httpx_async_client, + ): + yield + + +@pytest.fixture +def mock_redis(mocker): + with mocker.patch( + "wyvern.redis.wyvern_redis.mget", + side_effect=[ + [ + wyvern_encode(PRODUCT_ENTITY_1), + wyvern_encode(PRODUCT_ENTITY_2), + None, + wyvern_encode(USER_ENTITY), + ], + [ + wyvern_encode(BRAND_ENTITY_1), + wyvern_encode(BRAND_ENTITY_2), + ], + ], + ): + yield + + +@pytest.fixture +def mock_redis__duplicate_brand(mocker): + with mocker.patch( + "wyvern.redis.wyvern_redis.mget", + side_effect=[ + [ + wyvern_encode(PRODUCT_ENTITY_1), + wyvern_encode(PRODUCT_ENTITY_2__DUPLICATE_BRAND), + None, + wyvern_encode(USER_ENTITY), + ], + [ + wyvern_encode(BRAND_ENTITY_1), + wyvern_encode(BRAND_ENTITY_1), + ], + ], + ): + yield + + +class Query(WyvernEntity): + query: str + + def generate_identifier(self) -> Identifier: + return Identifier( + identifier=self.query, + identifier_type="query", + ) + + +class User(WyvernEntity): + user_id: str + name: Optional[str] + + def generate_identifier(self) -> Identifier: + return Identifier( + identifier=self.user_id, + identifier_type="user", + ) + + +class Brand(WyvernEntity): + brand_id: str + name: Optional[str] + + def generate_identifier(self) -> Identifier: + return Identifier( + identifier=self.brand_id, + identifier_type="brand", + ) + + +class Product(ProductEntity): + brand_id: Optional[str] + brand: Optional[Brand] + product_name: Optional[str] + product_description: Optional[str] + + def nested_hydration(self) -> Dict[str, str]: + """ + This method is used to hydrate nested entities. + """ + return { + "brand_id": "brand", + } + + +class ProductSearchRankingRequest( + BaseWyvernRequest, + CandidateSetEntity[Product], +): + query: Query + candidates: List[Product] + user: Optional[User] + + +class ProductSearchRankingResponse(BaseModel): + """ + The response schema for the ProductSearchRanking pipeline + """ + + success: str + + +class RankingModelComponent( + ModelComponent[ + ModelInput[Product, ProductSearchRankingRequest], + ModelOutput[float], + ], +): + @cached_property + def manifest_feature_names(self) -> Set[str]: + return { + "RankingRealtimeFeatureComponent:f_1", + "RankingRealtimeFeatureComponent:f_2", + "RankingRealtimeFeatureComponent:f_3", + "RankingRealtimeFeatureComponent:f_4", + "test_feature_view:test_batch_feature", + } + + async def inference( + self, + input: ModelInput[Product, ProductSearchRankingRequest], + **kwargs, + ) -> ModelOutput[float]: + return ModelOutput( + model_name="test_model", + data={ + entity.identifier: hash(entity.identifier.identifier) + for entity in input.entities + }, + ) + + +class RankingRealtimeFeatureComponent( + RealtimeFeatureComponent[Product, Any, ProductSearchRankingRequest], +): + def __init__(self): + super().__init__( + output_feature_names={ + "f_1", + "f_2", + "f_3", + "f_4", + }, + ) + + async def compute_features( + self, + entity: Product, + request: RealtimeFeatureRequest[ProductSearchRankingRequest], + ) -> Optional[FeatureData]: + return FeatureData( + identifier=entity.identifier, + features={ + "f_1": 1.2, + "f_2": 1.5, + "f_3": "string_value", + "f_4": [0, 1, 2, 3, 4, 5, 6, 7, 8], # embedding + }, + ) + + +class RankingComponent( + PipelineComponent[ProductSearchRankingRequest, ProductSearchRankingResponse], +): + PATH = "/product-search-ranking" + REQUEST_SCHEMA_CLASS = ProductSearchRankingRequest + RESPONSE_SCHEMA_CLASS = ProductSearchRankingResponse + + def __init__(self) -> None: + self.ranking_model_component = RankingModelComponent() + super().__init__(self.ranking_model_component) + + async def execute( + self, + input: ProductSearchRankingRequest, + **kwargs, + ) -> ProductSearchRankingResponse: + assert input.user and input.user.name == "test user name" + assert input.candidates[0].product_name == "test_product1 name" + assert input.candidates[0].product_description == "test_product1_description" + assert input.candidates[0].brand_id == "test_brand1" + assert input.candidates[0].brand is not None + assert input.candidates[0].brand.brand_id == "test_brand1" + assert input.candidates[0].brand.name == "test brand1 name" + assert input.candidates[1].product_name == "test_product2 name" + assert input.candidates[1].product_description == "test_product2_description" + assert input.candidates[1].brand_id == "test_brand2" + assert input.candidates[1].brand is not None + assert input.candidates[1].brand.brand_id == "test_brand2" + assert input.candidates[1].brand.name == "test brand2 name" + + # model inference + model_output = await self.ranking_model_component.execute( + input=ModelInput( + request=input, + entities=input.candidates, + ), + ) + assert model_output.model_name == "test_model" + assert model_output.data[input.candidates[0].identifier] == float( + hash(input.candidates[0].identifier.identifier), + ) + assert model_output.data[input.candidates[1].identifier] == float( + hash(input.candidates[1].identifier.identifier), + ) + return ProductSearchRankingResponse(success="success") + + +@pytest.fixture +def test_client(mock_redis): + wyvern_service = WyvernService.generate( + route_components=[RankingComponent], + ) + yield TestClient(wyvern_service.service.app) + + +def test_get_all_identifiers(): + request = ProductSearchRankingRequest.parse_obj( + { + "request_id": "test_request_id", + "query": { + "query": "test query", + }, + "candidates": [ + { + "product_id": "test_product1", + }, + { + "product_id": "test_product2", + }, + ], + "user": { + "user_id": "test_user", + }, + }, + ) + + assert set(request.get_all_identifiers()) == set( + [ + Identifier(identifier="test_product1", identifier_type="product"), + Identifier(identifier="test_product2", identifier_type="product"), + Identifier(identifier="test query", identifier_type="query"), + Identifier(identifier="test_user", identifier_type="user"), + ], + ) + + +@pytest.mark.asyncio +async def test_hydrate(mock_redis): + json_input = ProductSearchRankingRequest.parse_obj( + { + "request_id": "test_request_id", + "query": { + "query": "test query", + }, + "candidates": [ + { + "product_id": "test_product1", + }, + { + "product_id": "test_product2", + }, + ], + "user": { + "user_id": "test_user", + }, + }, + ) + # set up wyvern request context + test_wyvern_request = WyvernRequest( + method="GET", + url="http://test.com", + url_path="/", + json=json_input, + headers={}, + entity_store={}, + events=[], + feature_map=FeatureMap(feature_map={}), + ) + request_context.set(test_wyvern_request) + + # mock redis mget + + # call component.hydrate + component = RankingComponent() + await component.hydrate(json_input) + + # assertion: all entities are hydrated correctly + assert json_input.user and json_input.user.name == "test user name" + assert json_input.candidates[0].product_name == "test_product1 name" + assert json_input.candidates[0].product_description == "test_product1_description" + assert json_input.candidates[0].brand_id == "test_brand1" + assert json_input.candidates[0].brand is not None + assert json_input.candidates[0].brand.brand_id == "test_brand1" + assert json_input.candidates[0].brand.name == "test brand1 name" + assert json_input.candidates[1].product_name == "test_product2 name" + assert json_input.candidates[1].product_description == "test_product2_description" + assert json_input.candidates[1].brand_id == "test_brand2" + assert json_input.candidates[1].brand is not None + assert json_input.candidates[1].brand.brand_id == "test_brand2" + assert json_input.candidates[1].brand.name == "test brand2 name" + all_entities = json_input.get_all_entities() + assert len(all_entities) == 6 + + # reset wyvern request context + request_context.reset() + + +@pytest.mark.asyncio +async def test_hydrate__duplicate_brand(mock_redis__duplicate_brand): + json_input = ProductSearchRankingRequest.parse_obj( + { + "request_id": "test_request_id", + "query": { + "query": "test query", + }, + "candidates": [ + { + "product_id": "test_product1", + }, + { + "product_id": "test_product2", + }, + ], + "user": { + "user_id": "test_user", + }, + }, + ) + # set up wyvern request context + test_wyvern_request = WyvernRequest( + method="GET", + url="http://test.com", + url_path="/", + json=json_input, + headers={}, + entity_store={}, + events=[], + feature_map=FeatureMap(feature_map={}), + ) + request_context.set(test_wyvern_request) + + # mock redis mget + + # call component.hydrate + component = RankingComponent() + await component.hydrate(json_input) + + # assertion: all entities are hydrated correctly + assert json_input.user and json_input.user.name == "test user name" + assert json_input.candidates[0].product_name == "test_product1 name" + assert json_input.candidates[0].product_description == "test_product1_description" + assert json_input.candidates[0].brand_id == "test_brand1" + assert json_input.candidates[0].brand is not None + assert json_input.candidates[0].brand.brand_id == "test_brand1" + assert json_input.candidates[0].brand.name == "test brand1 name" + assert json_input.candidates[1].product_name == "test_product2 name" + assert json_input.candidates[1].product_description == "test_product2_description" + assert json_input.candidates[1].brand_id == "test_brand1" + assert json_input.candidates[1].brand is not None + assert json_input.candidates[1].brand.brand_id == "test_brand1" + assert json_input.candidates[1].brand.name == "test brand1 name" + all_entities = json_input.get_all_entities() + assert len(all_entities) == 5 + + # reset wyvern request context + request_context.reset() + + +@pytest.mark.asyncio +async def test_end_to_end(mock_redis, mock_httpx_post, test_client): + response = test_client.post( + "/api/v1/product-search-ranking", + json={ + "request_id": "test_request_id", + "query": { + "query": "test query", + }, + "candidates": [ + { + "product_id": "test_product1", + }, + { + "product_id": "test_product2", + }, + ], + "user": { + "user_id": "test_user", + }, + }, + ) + assert response.status_code == 200 + assert response.json() == {"success": "success"} diff --git a/wyvern/__init__.py b/wyvern/__init__.py new file mode 100644 index 0000000..6f93100 --- /dev/null +++ b/wyvern/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from wyvern.wyvern_logging import setup_logging +from wyvern.wyvern_tracing import setup_tracing + +setup_logging() +setup_tracing() diff --git a/wyvern/api.py b/wyvern/api.py new file mode 100644 index 0000000..ae1f993 --- /dev/null +++ b/wyvern/api.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +import asyncio +from typing import Any, Dict, Hashable, List, Optional, Union + +import httpx +import nest_asyncio +import pandas as pd +import requests +from pandas.api.types import is_datetime64_any_dtype +from tqdm import tqdm + +from wyvern.config import settings +from wyvern.exceptions import WyvernAPIKeyMissingError, WyvernError + +BATCH_SIZE = 15000 +HTTP_TIMEOUT = 60 +BATCH_SIZE_PER_GATHER = 4 + + +class WyvernAPI: + def __init__( + self, + api_key: Optional[str] = None, + base_url: Optional[str] = None, + batch_size: int = BATCH_SIZE, + ) -> None: + api_key = api_key or settings.WYVERN_API_KEY + if not api_key: + raise WyvernAPIKeyMissingError() + self.headers = {"x-api-key": api_key} + self.base_url = base_url or settings.WYVERN_BASE_URL + self.batch_size = batch_size + self.async_client = httpx.AsyncClient(timeout=HTTP_TIMEOUT) + + def get_online_features( + self, + features: List[str], + entities: Dict[str, List[Any]], + get_event_timestamps: bool = False, + get_feature_statuses: bool = False, + ) -> pd.DataFrame: + request = {"features": features, "entities": entities} + response = self._send_request_to_wyvern_api( + settings.WYVERN_ONLINE_FEATURES_PATH, + data=request, + ) + + return self._convert_online_features_to_df( + response, + get_feature_statuses, + get_event_timestamps, + ) + + def get_historical_features( + self, + features: List[str], + entities: Union[Dict[Hashable, List[Any]], pd.DataFrame], + ) -> pd.DataFrame: + # nest_asyncio call is needed to avoid RuntimeError: This event loop is already running with notebook use case + nest_asyncio.apply() + if isinstance(entities, dict): + entities = pd.DataFrame(entities) + + # build the new name to old name mapping + lower_case_column_to_old_column: Dict[str, str] = { + column.lower(): column for column in entities.columns + } + lower_case_feature_to_old_feature: Dict[str, str] = { + feature.replace(":", "__", 1).lower(): feature for feature in features + } + + # all entities name will be lower case when passing to the wyvern API + entities = entities.rename(columns=str.lower) + + for column in entities.columns: + if is_datetime64_any_dtype(entities[column]): + entities[column] = entities[column].astype(str) + + entities = entities.to_dict(orient="list") + + # validate data first + # 1. make sure "request" is in entities + if "request" not in entities: + raise ValueError("entities must contain 'request' key") + if "timestamp" not in entities: + raise ValueError("entities must contain 'timestamp' key") + timestamps = entities["timestamp"] + timestamps_length = len(timestamps) + for key, value in entities.items(): + if len(value) != timestamps_length: + raise ValueError( + f"Length of entity '{key}' ({len(value)}) does " + f"not match length of timestamp ({timestamps_length})", + ) + # Split data into batches with up to 2000 items per batch + num_batches = (timestamps_length + self.batch_size - 1) // self.batch_size + data_batches = [] + for i in range(num_batches): + start_idx = i * self.batch_size + end_idx = min((i + 1) * self.batch_size, timestamps_length) + batch_entities = { + key: value[start_idx:end_idx] for key, value in entities.items() + } + batch_timestamps = timestamps[start_idx:end_idx] + data_batches.append( + { + "features": features, + "entities": batch_entities, + "timestamps": batch_timestamps, + }, + ) + + # Send requests for each batch and concatenate the responses + responses: list[pd.DataFrame] = [] + num_gathers = (num_batches + BATCH_SIZE_PER_GATHER - 1) // BATCH_SIZE_PER_GATHER + progress_bar = tqdm( + total=len(data_batches), + desc="Fetching historical data", + unit="batch", + ) + event_loop = _get_event_loop() + for i in range(num_gathers): + start_idx = i * BATCH_SIZE_PER_GATHER + end_idx = min((i + 1) * BATCH_SIZE_PER_GATHER, num_batches) + gathered_responses = event_loop.run_until_complete( + self.process_batches(data_batches[start_idx:end_idx]), + ) + for response in gathered_responses: + responses.append( + self._convert_historical_features_to_df(response), + ) + progress_bar.update(end_idx - start_idx) + + # Concatenate the responses into a single DataFrame + result_df = pd.concat(responses) + progress_bar.close() + + def get_old_name(new_name: str) -> str: + lower_case_name = new_name.lower() + if lower_case_name in lower_case_column_to_old_column: + return lower_case_column_to_old_column[lower_case_name] + if lower_case_name in lower_case_feature_to_old_feature: + return lower_case_feature_to_old_feature[lower_case_name] + return new_name + + result_df = result_df.rename(columns=get_old_name) + + result_df[lower_case_column_to_old_column["timestamp"]] = pd.to_datetime( + result_df[lower_case_column_to_old_column["timestamp"]], + ) + + return result_df.rename(columns=get_old_name) + + async def process_batches(self, batches: list[dict]) -> list[dict]: + return await asyncio.gather( + *[ + self._send_request_to_wyvern_api_async( + settings.WYVERN_HISTORICAL_FEATURES_PATH, + batch, + ) + for batch in batches + ], + ) + + def _send_request_to_wyvern_api( + self, + api_path: str, + data: Dict[str, Any], + ) -> Dict[str, Any]: + url = f"{self.base_url}{api_path}" + response = requests.post(url, headers=self.headers, json=data) + + if response.status_code != 200: + self._handle_failed_request(response) + + return response.json() + + async def _send_request_to_wyvern_api_async( + self, + api_path: str, + data: Dict[str, Any], + ) -> Dict[str, Any]: + url = f"{self.base_url}{api_path}" + response = await self.async_client.post(url, headers=self.headers, json=data) + + if response.status_code != 200: + self._handle_failed_request(response) + + return response.json() + + def _handle_failed_request( + self, + response: Union[httpx.Response, requests.Response], + ) -> None: + raise WyvernError(f"Request failed [{response.status_code}]: {response.json()}") + + def _convert_online_features_to_df( + self, + data, + get_event_timestamps: bool = False, + get_feature_statuses: bool = False, + ) -> pd.DataFrame: + df_dict = {} + feature_names = data["metadata"]["feature_names"] + for i, feature in enumerate(feature_names): + result = data["results"][i] + df_dict[feature] = result["values"] + if get_event_timestamps: + df_dict[feature + "_event_timestamps"] = result["event_timestamps"] + if get_feature_statuses: + df_dict[feature + "_statuses"] = result["statuses"] + + return pd.DataFrame(df_dict) + + def _convert_historical_features_to_df( + self, + data, + ) -> pd.DataFrame: + df = pd.DataFrame(data["results"]) + return df + + +def _get_event_loop(): + try: + return asyncio.get_running_loop() + except RuntimeError: + return asyncio.new_event_loop() diff --git a/wyvern/aws/__init__.py b/wyvern/aws/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/aws/kinesis.py b/wyvern/aws/kinesis.py new file mode 100644 index 0000000..b305ebd --- /dev/null +++ b/wyvern/aws/kinesis.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +import logging +import traceback +from enum import Enum +from typing import Callable, List + +import boto3 +from ddtrace import tracer +from pydantic import BaseModel + +from wyvern.config import settings + +logger = logging.getLogger(__name__) + +CHUNK_SIZE = 100 + + +class KinesisFirehoseStream(str, Enum): + EVENT_STREAM = "event-stream" + + def get_stream_name( + self, + customer_specific: bool = True, + env_specific: bool = True, + ) -> str: + stream_name = self.value + if customer_specific: + stream_name = f"{settings.PROJECT_NAME}-{stream_name}" + + if env_specific: + # env_name = "production" if settings.ENVIRONMENT == "development" else settings.ENVIRONMENT + env_name = settings.ENVIRONMENT + stream_name = f"{stream_name}-{env_name}" + + return stream_name + + +class WyvernKinesisFirehose: + def __init__(self): + self.firehose_client = boto3.client( + "firehose", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + region_name=settings.AWS_REGION_NAME, + ) + + def put_record_batch_callable( + self, + stream_name: KinesisFirehoseStream, + record_generator: List[Callable[[], List[BaseModel]]], + ): + with tracer.trace("flush_records_to_kinesis_firehose"): + records = [ + record + for record_generator in record_generator + for record in record_generator() + ] + self.put_record_batch(stream_name, records) + + def put_record_batch( + self, + stream_name: KinesisFirehoseStream, + records: List[BaseModel], + ): + if not records: + return + logger.info( + f"Sending {len(records)} records to {stream_name.get_stream_name()}", + ) + dict_records = [{"Data": record.json()} for record in records] + + record_chunks = [ + dict_records[i : (i + CHUNK_SIZE)] + for i in range(0, len(dict_records), CHUNK_SIZE) + ] + for chunk in record_chunks: + if settings.ENVIRONMENT == "development": + logger.info( + "Not sending records to Kinesis Firehose in development mode.", + ) + logger.debug(f"Records: {chunk}") + else: + try: + self.firehose_client.put_record_batch( + DeliveryStreamName=stream_name.get_stream_name(), + Records=chunk, + ) + except Exception: + logger.exception( + "Failed to put records to kinesis firehose", + traceback.format_exc(), + ) + + +wyvern_kinesis_firehose = WyvernKinesisFirehose() diff --git a/wyvern/clients/__init__.py b/wyvern/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/clients/snowflake.py b/wyvern/clients/snowflake.py new file mode 100644 index 0000000..5a91a1c --- /dev/null +++ b/wyvern/clients/snowflake.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import snowflake.connector + +from wyvern.config import settings + + +def generate_snowflake_ctx() -> snowflake.connector.SnowflakeConnection: + return snowflake.connector.connect( + user=settings.SNOWFLAKE_USER, + password=settings.SNOWFLAKE_PASSWORD, + role=settings.SNOWFLAKE_ROLE, + account=settings.SNOWFLAKE_ACCOUNT, + warehouse=settings.SNOWFLAKE_WAREHOUSE, + database=settings.SNOWFLAKE_DATABASE, + schema=settings.SNOWFLAKE_OFFLINE_STORE_SCHEMA, + ) diff --git a/wyvern/components/__init__.py b/wyvern/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/api_route_component.py b/wyvern/components/api_route_component.py new file mode 100644 index 0000000..9a68ce4 --- /dev/null +++ b/wyvern/components/api_route_component.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +from collections import deque +from typing import Deque, List, Tuple, Type + +from ddtrace import tracer + +from wyvern import request_context +from wyvern.components.component import Component +from wyvern.entities.identifier import Identifier +from wyvern.entities.identifier_entities import WyvernDataModel, WyvernEntity +from wyvern.redis import wyvern_redis +from wyvern.wyvern_typing import REQUEST_SCHEMA, RESPONSE_SCHEMA + + +class APIRouteComponent(Component[REQUEST_SCHEMA, RESPONSE_SCHEMA]): + # this is the api version + API_VERSION: str = "v1" + + # this is the path for the API route + PATH: str + # this is the class of request schema represented by pydantic BaseModel + REQUEST_SCHEMA_CLASS: Type[REQUEST_SCHEMA] + # this is the class of response schema represented by pydantic BaseModel + RESPONSE_SCHEMA_CLASS: Type[RESPONSE_SCHEMA] + + async def warm_up(self, input: REQUEST_SCHEMA) -> None: + # TODO shu: hydrate + await self.hydrate(input) + return + + @tracer.wrap(name="APIRouteComponent.hydrate") + async def hydrate(self, input: REQUEST_SCHEMA) -> None: + """ + Wyvern APIRouteComponent recursively hydrate the request input data with Wyvern Index data + + TODO: this function could be moved to a global place + """ + if not isinstance(input, WyvernDataModel): + return + # use BFS to go through the input pydantic model + # hydrate the data for each WyvernEntity that is encountered layer by layer if there are nested WyvernEntity + identifiers: List[Identifier] = input.get_all_identifiers(cached=False) + queue: Deque[WyvernDataModel] = deque([input]) + while identifiers and queue: + identifiers, queue = await self._bfs_hydrate(identifiers, queue) + + async def _bfs_hydrate( + self, + identifiers: List[Identifier], + queue: Deque[WyvernDataModel], + ) -> Tuple[List[Identifier], Deque[WyvernDataModel]]: + current_request = request_context.ensure_current_request() + + # load all the entities from Wyvern Index to self.entity_store + index_keys = [identifier.index_key() for identifier in identifiers] + + # we're doing an in place update for the entity_store here to save redundant iterations + # and improve the performance of the code + await wyvern_redis.mget_update_in_place(index_keys, current_request) + + next_level_queue: Deque[WyvernDataModel] = deque([]) + next_level_identifiers: List[Identifier] = [] + while queue: + current_obj = queue.popleft() + # go through all the fields of the current object, and add WyvernDataModel to the queue + for field in current_obj.__fields__: + value = getattr(current_obj, field) + if isinstance(value, WyvernDataModel): + queue.append(value) + if isinstance(value, List): + # if the field is a list, we need to check each item in the list + # to make sure WyvernDataModel items are enqueued + for item in value: + if isinstance(item, WyvernDataModel): + queue.append(item) + + if isinstance(current_obj, WyvernEntity): + # if the current node is a WyvernEntity, + # we need to hydrate the data if the entity exists in the index + # get the entity from wyvern index + index_key = current_obj.identifier.index_key() + entity = current_request.entity_store.get(index_key) + + # load the data into the entity + if entity: + current_obj.load_fields(entity) + + # generate the next level queue + for ( + id_field_name, + entity_field_name, + ) in current_obj.nested_hydration().items(): + id_field_value = getattr(current_obj, id_field_name) + if not id_field_value: + continue + entity_class: Type[WyvernEntity] = current_obj.__fields__[ + entity_field_name + ].type_ + entity_obj = entity_class(**{id_field_name: id_field_value}) + setattr( + current_obj, + entity_field_name, + entity_obj, + ) + next_level_identifiers.append(entity_obj.identifier) + next_level_queue.append(getattr(current_obj, entity_field_name)) + return next_level_identifiers, next_level_queue diff --git a/wyvern/components/business_logic/__init__.py b/wyvern/components/business_logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/business_logic/boosting_business_logic.py b/wyvern/components/business_logic/boosting_business_logic.py new file mode 100644 index 0000000..957f5ee --- /dev/null +++ b/wyvern/components/business_logic/boosting_business_logic.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging +from abc import abstractmethod +from typing import Callable, Dict, Generic, List, Set + +import pandas as pd +from pandas import DataFrame, Series + +from wyvern.components.business_logic.business_logic import ( + BusinessLogicComponent, + BusinessLogicRequest, +) +from wyvern.components.component import Component +from wyvern.entities.candidate_entities import ( + GENERALIZED_WYVERN_ENTITY, + ScoredCandidate, +) +from wyvern.wyvern_typing import REQUEST_ENTITY + +logger = logging.getLogger(__name__) + + +class BoostingBusinessLogicComponent( + BusinessLogicComponent[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + """ + A component that performs boosting on an entity with a set of candidates + + The request itself could contain more than just entities, for example it may contain a query and so on + """ + + def __init__(self, *upstreams: Component): + super().__init__(*upstreams, name=self.__class__.__name__) + + def boost( + self, + scored_candidates: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]], + entity_keys: Set[str], + boost: float, + entity_key_mapping: Callable[ + [GENERALIZED_WYVERN_ENTITY], + str, + ] = lambda candidate: candidate.identifier.identifier, + multiplicative=False, + ) -> List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]]: + """ + Boosts the score of each candidate by a certain factor + + :param scored_candidates: The list of scored candidates + :param entity_keys: The set of entity keys (unique identifiers) to boost + :param boost: The boost factor + :param entity_key_mapping: A lambda function that takes in a candidate entity and + returns the field we should apply the boost to + :param multiplicative: Whether to apply the boost multiplicatively or additively + """ + + return [ + self._apply_boost_if_applicable( + boost, + candidate, + entity_key_mapping, + entity_keys, + multiplicative, + ) + for candidate in scored_candidates + ] + + def _apply_boost_if_applicable( + self, + boost: float, + candidate: ScoredCandidate[GENERALIZED_WYVERN_ENTITY], + entity_key_mapping: Callable[[GENERALIZED_WYVERN_ENTITY], str], + entity_keys: Set[str], + multiplicative: bool, + ) -> ScoredCandidate[GENERALIZED_WYVERN_ENTITY]: + if entity_key_mapping(candidate.entity) in entity_keys: + # TODO (suchintan): Should this be done in-place instead? + new_score = ( + candidate.score * boost if multiplicative else candidate.score + boost + ) + + return ScoredCandidate(entity=candidate.entity, score=new_score) + else: + return candidate + + +# TODO (suchintan): Add a boost key class +# class BoostKey(BaseModel): + + +class CSVBoostingBusinessLogicComponent( + BoostingBusinessLogicComponent[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + def __init__(self, *upstreams, csv_file: str, multiplicative: bool = False): + """ + This component reads a csv file and applies the boost based on + specific column name, entity key, and score combinations + + Methods to define: Given a CSV row, generate the entity key and boost value + """ + self.csv_file = csv_file + self.parsed_file: DataFrame = pd.DataFrame() + self.lookup: Dict[str, float] = {} + self.multiplicative = multiplicative + super().__init__(*upstreams) + + async def initialize(self) -> None: + """ + Initialize the component + """ + self.parsed_file = pd.read_csv(self.csv_file) + + for _index, row in self.parsed_file.iterrows(): + key = await self.extract_keys_from_csv_row(row) + boost = await self.extract_boost_value_from_csv_row(row) + # TODO (suchintan): Handle collissions + self.lookup[key] = boost + + @abstractmethod + async def extract_keys_from_csv_row(self, row: Series) -> str: + """ + Given a CSV row, generate the unique combinations that would apply a boost + + Example, in a file that has the following: + product_id, query, boost + + The method would return a unique concatenation (ie product_id:query) + """ + ... + + @abstractmethod + async def extract_boost_value_from_csv_row(self, row: Series) -> float: + """ + Given a CSV row, generate the unique combinations that would apply a boost + + Example, in a file that has the following: + product_id, query, boost + + The method would return the boost value + """ + ... + + @abstractmethod + async def extract_key_from_request_entity( + self, + candidate: GENERALIZED_WYVERN_ENTITY, + request: REQUEST_ENTITY, + ) -> str: + """ + Given a candidate and a request, generate a unique key that would apply a boost + """ + ... + + async def execute( + self, + input: BusinessLogicRequest[ + GENERALIZED_WYVERN_ENTITY, + REQUEST_ENTITY, + ], + **kwargs, + ) -> List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]]: + """ + Boosts the score of each candidate by a certain factor + """ + re_scored_candidates: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]] = [] + for candidate in input.scored_candidates: + key = await self.extract_key_from_request_entity( + candidate.entity, + input.request, + ) + if key in self.lookup: + new_score = ( + candidate.score * self.lookup[key] + if self.multiplicative + else candidate.score + self.lookup[key] + ) + re_scored_candidates.append( + ScoredCandidate( + entity=candidate.entity, + score=new_score, + ), + ) + else: + re_scored_candidates.append(candidate) + + return re_scored_candidates diff --git a/wyvern/components/business_logic/business_logic.py b/wyvern/components/business_logic/business_logic.py new file mode 100644 index 0000000..c87460a --- /dev/null +++ b/wyvern/components/business_logic/business_logic.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging +from datetime import datetime +from typing import Generic, List + +from ddtrace import tracer +from pydantic.generics import GenericModel + +from wyvern import request_context +from wyvern.components.component import Component +from wyvern.components.events.events import EntityEventData, EventType, LoggedEvent +from wyvern.components.helpers.sorting import SortingComponent +from wyvern.entities.candidate_entities import ( + GENERALIZED_WYVERN_ENTITY, + ScoredCandidate, +) +from wyvern.event_logging import event_logger +from wyvern.wyvern_typing import REQUEST_ENTITY + +logger = logging.getLogger(__name__) + + +class BusinessLogicEventData(EntityEventData): + business_logic_pipeline_order: int + business_logic_name: str + old_score: float + new_score: float + + +class BusinessLogicEvent(LoggedEvent[BusinessLogicEventData]): + event_type: EventType = EventType.BUSINESS_LOGIC + + +class BusinessLogicRequest( + GenericModel, + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + request: REQUEST_ENTITY + scored_candidates: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]] + + # TODO (suchintan): Give business logic layer access to the feature map in the future + # feature_map: FeatureMap + + +# TODO (suchintan): Possibly delete this now that events are gone +class BusinessLogicResponse( + GenericModel, + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + request: BusinessLogicRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY] + adjusted_candidates: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]] + + +class BusinessLogicComponent( + Component[ + BusinessLogicRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]], + ], + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + """ + A component that performs business logic on an entity with a set of candidates + + The request itself could contain more than just entities, for example it may contain a query and so on + """ + + pass + + +class BusinessLogicPipeline( + Component[ + BusinessLogicRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + BusinessLogicResponse[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + ], + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + """ + Steps through a series of business logic components and returns the final output + + This operation is fully chained, meaning that the output of each business logic component is passed + as an input to the next business logic component + """ + + def __init__( + self, + *upstreams: BusinessLogicComponent[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + name: str, + ): + self.ordered_upstreams = upstreams + self.sorting_component: SortingComponent = SortingComponent( + name="business_logic_sorting", + ) + super().__init__(self.sorting_component, *upstreams, name=name) + + @tracer.wrap(name="BusinessLogicPipeline.execute") + async def execute( + self, + input: BusinessLogicRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + **kwargs, + ) -> BusinessLogicResponse[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY]: + argument = input + + # Make sure that the inputted candidates are actually sorted + output = await self.sorting_component.execute(input.scored_candidates) + + for (pipeline_index, upstream) in enumerate(self.ordered_upstreams): + old_scores = [candidate.score for candidate in argument.scored_candidates] + + # this output might have the same reference as the argument.scored_candidates + output = await upstream.execute(input=argument, **kwargs) + + # TODO (suchintan): This currently assumes that the + # output order is unchanged by the business logic component + # This is not necessarily true, so we should fix this in the future + + # TODO (suchintan): Make this properly async -- right now it's fast enough + # where we don't have to care, but in the future we might + extracted_events: List[ + BusinessLogicEvent + ] = self.extract_business_logic_events( + output, + pipeline_index, + upstream.name, + argument.request.request_id, + old_scores, + ) + + def log_events( + extracted_events: List[BusinessLogicEvent] = extracted_events, + ): + return extracted_events + + # TODO (suchintan): "invariant" list error + event_logger.log_events(log_events) # type: ignore + output = await self.sorting_component.execute(output) + + argument = BusinessLogicRequest( + request=input.request, + scored_candidates=output, + ) + + return BusinessLogicResponse( + request=input, + adjusted_candidates=output, + ) + + def extract_business_logic_events( + self, + output: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]], + pipeline_index: int, + upstream_name: str, + request_id: str, + old_scores: List[float], + ) -> List[BusinessLogicEvent]: + timestamp = datetime.utcnow() + events = [ + BusinessLogicEvent( + request_id=request_id, + api_source=request_context.ensure_current_request().url_path, + event_timestamp=timestamp, + event_data=BusinessLogicEventData( + business_logic_pipeline_order=pipeline_index, + business_logic_name=upstream_name, + old_score=old_scores[j], + new_score=output[j].score, + entity_identifier=candidate.entity.identifier.identifier, + entity_identifier_type=candidate.entity.identifier.identifier_type, + ), + ) + for (j, candidate) in enumerate(output) + if output[j].score != old_scores[j] + ] + + return events diff --git a/wyvern/components/business_logic/pinning_business_logic.py b/wyvern/components/business_logic/pinning_business_logic.py new file mode 100644 index 0000000..aa97467 --- /dev/null +++ b/wyvern/components/business_logic/pinning_business_logic.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +import logging +from typing import Callable, Dict, Generic, List, Optional + +from wyvern.components.business_logic.business_logic import BusinessLogicComponent +from wyvern.components.component import Component +from wyvern.entities.candidate_entities import ( + GENERALIZED_WYVERN_ENTITY, + ScoredCandidate, +) +from wyvern.wyvern_typing import REQUEST_ENTITY + +logger = logging.getLogger(__name__) + + +class PinningBusinessLogicComponent( + BusinessLogicComponent[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + """ + A component that performs boosting on an entity with a set of candidates + + The request itself could contain more than just entities, for example it may contain a query and so on + """ + + def __init__(self, *upstreams: Component): + super().__init__(*upstreams, name=self.__class__.__name__) + + def pin( + self, + scored_candidates: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]], + entity_pins: Dict[str, int], + entity_key_mapping: Callable[ + [GENERALIZED_WYVERN_ENTITY], + str, + ] = lambda candidate: candidate.identifier.identifier, + allow_down_ranking: bool = False, + ) -> List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]]: + """ + Pins the supplied entity to the specific position + + :param scored_candidates: The list of scored candidates + :param entity_pins: The map of entity keys (unique identifiers) to pin, and their pinning position + :param entity_key_mapping: A lambda function that takes in a candidate entity and + returns the field we should apply the pin to + :param allow_down_ranking: Whether to allow down-ranking of candidates that are not pinned + """ + + applied_pins_score: Dict[int, float] = {} + re_scored_candidates: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]] = [] + for index, candidate in enumerate(scored_candidates): + current_position = len(scored_candidates) - index + pin_candidate_new_position: Optional[int] = None + # Determine which desired pins are applicable for this candidate set + entity_key = entity_key_mapping(candidate.entity) + if entity_key in entity_pins: + desired_position = entity_pins[entity_key] + + if allow_down_ranking or desired_position < current_position: + pin_candidate_new_position = min( + desired_position, + len(scored_candidates) - 1, + ) + + if pin_candidate_new_position is None: + re_scored_candidates.append(candidate) + continue + + pinned_score = self._get_pinned_score( + applied_pins_score, + candidate, + pin_candidate_new_position, + scored_candidates, + ) + + re_scored_candidates.append( + ScoredCandidate(entity=candidate.entity, score=pinned_score), + ) + + self._update_applied_pins_score( + applied_pins_score, + pin_candidate_new_position, + pinned_score, + ) + + return re_scored_candidates + + def _update_applied_pins_score( + self, + applied_pins_score, + current_position, + new_score, + ): + if current_position in applied_pins_score: + # This means this position already had a pin applied to it.. so we need to update the position + existing_pin_score = applied_pins_score[current_position] + if existing_pin_score > new_score: + # new_score is smaller, so let's update our memory of the pin to occupy the previous position + self._update_applied_pins_score( + applied_pins_score, + current_position - 1, + existing_pin_score, + ) + else: + # new_score is higher, so let's update our memory of the pin to occupy the next position + self._update_applied_pins_score( + applied_pins_score, + current_position + 1, + existing_pin_score, + ) + + applied_pins_score[current_position] = new_score + + def _get_pinned_score( + self, + applied_pins_score, + candidate, + pin_candidate_new_position, + scored_candidates, + ): + if pin_candidate_new_position >= len(scored_candidates) - 1: + # Pinned position is outside or at the bottom of the candidate set, + # subtract current score from the lowest score + # If there are multiple pins at the bottom, it will respect their relative score + # The reciprocal is used to ensure that higher scored products end up having a higher final score + return scored_candidates[-1].score - (1.0 / candidate.score) + elif pin_candidate_new_position == 0: + # Pinned position is at the top of the candidate set -- add the highest score to the current candidate score + # If there are multiple pins at position 1, it will currently respect their relative score + # This makes sense to me, but we can change it if we want + return scored_candidates[0].score + candidate.score + else: + # Average the scores of the candidates on either side of the pinned position + left_position = pin_candidate_new_position + right_position = pin_candidate_new_position + 1 + left_side_score = ( + scored_candidates[left_position].score + if left_position not in applied_pins_score + else applied_pins_score[left_position] + ) + right_side_score = ( + scored_candidates[right_position].score + if right_position not in applied_pins_score + else applied_pins_score[right_position] + ) + logger.debug( + f"applied_pins_score={applied_pins_score} candidate={candidate.entity.get_all_identifiers()} " + f"pin_candidate_new_position={pin_candidate_new_position} " + f"left_side_score={left_side_score} right_side_score={right_side_score}", + ) + return (left_side_score + right_side_score) / 2 diff --git a/wyvern/components/candidates/__init__.py b/wyvern/components/candidates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/candidates/candidate_logger.py b/wyvern/components/candidates/candidate_logger.py new file mode 100644 index 0000000..8266bf3 --- /dev/null +++ b/wyvern/components/candidates/candidate_logger.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from typing import Generic, List + +from ddtrace import tracer +from pydantic.generics import GenericModel + +from wyvern import request_context +from wyvern.components.component import Component +from wyvern.components.events.events import EntityEventData, EventType, LoggedEvent +from wyvern.entities.candidate_entities import ( + GENERALIZED_WYVERN_ENTITY, + ScoredCandidate, +) +from wyvern.event_logging import event_logger +from wyvern.wyvern_typing import REQUEST_ENTITY + + +class CandidateEventData(EntityEventData): + candidate_score: float + candidate_order: int + + +class CandidateEvent(LoggedEvent[CandidateEventData]): + event_type: EventType = EventType.CANDIDATE + + +class CandidateEventLoggingRequest( + GenericModel, + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + request: REQUEST_ENTITY + scored_candidates: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]] + + +class CandidateEventLoggingComponent( + Component[ + CandidateEventLoggingRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + None, + ], + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + @tracer.wrap(name="CandidateEventLoggingComponent.execute") + async def execute( + self, + input: CandidateEventLoggingRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + **kwargs + ) -> None: + current_span = tracer.current_span() + if current_span: + current_span.set_tag("candidate_size", len(input.scored_candidates)) + url_path = request_context.ensure_current_request().url_path + + def candidate_events_generator() -> List[CandidateEvent]: + timestamp = datetime.utcnow() + candidate_events = [ + CandidateEvent( + request_id=input.request.request_id, + api_source=url_path, + event_timestamp=timestamp, + event_data=CandidateEventData( + entity_identifier=candidate.entity.identifier.identifier, + entity_identifier_type=candidate.entity.identifier.identifier_type, + candidate_score=candidate.score, + candidate_order=i, + ), + ) + for i, candidate in enumerate(input.scored_candidates) + ] + return candidate_events + + event_logger.log_events(candidate_events_generator) # type: ignore diff --git a/wyvern/components/component.py b/wyvern/components/component.py new file mode 100644 index 0000000..867d671 --- /dev/null +++ b/wyvern/components/component.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import asyncio +import logging +from enum import Enum +from functools import cached_property +from typing import Dict, Generic, Optional, Set +from uuid import uuid4 + +from wyvern import request_context +from wyvern.entities.identifier import Identifier +from wyvern.wyvern_typing import INPUT_TYPE, OUTPUT_TYPE, WyvernFeature + +logger = logging.getLogger(__name__) + + +class ComponentStatus(str, Enum): + created = "created" + initialized = "initialized" + failed = "failed" + + +class Component(Generic[INPUT_TYPE, OUTPUT_TYPE]): + def __init__( + self, + *upstreams: Component, + name: Optional[str] = None, + ) -> None: + self._name = name or self.__class__.__name__ + self._id = uuid4() + self.upstreams: Dict[str, Component] = { + upstream.name: upstream for upstream in upstreams + } + self._status: ComponentStatus = ComponentStatus.created + + self.initialized_event = asyncio.Event() + self.initialized_components: Set[Component] = set() + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.__repr__() + + def __hash__(self) -> int: + return hash(self._id) + + def __eq__(self, other: object) -> bool: + return isinstance(other, Component) and self._id == other._id + + @property + def name(self) -> str: + return self._name + + @property + def status(self) -> ComponentStatus: + return self._status + + @status.setter + def status(self, value: ComponentStatus) -> None: + self._status = value + + async def initialize(self) -> None: + """ + This is the place where you can do some initialization work for your component + + As an example, you can initialize a model here or load a file, + which is needed for your component to work + """ + return None + + async def initialize_wrapper(self) -> None: + """ + Extend this method if your component has some work that needs to be done on server startup + + This is a great place to initialize libraries to access external libraries, warm up models, etc + + This runs after all objects have been constructed + """ + try: + uninitialized_components = [ + component + for component in self.upstreams.values() + if component not in self.initialized_components + ] + await asyncio.gather( + *[ + component.initialize_wrapper() + for component in uninitialized_components + ], + # This forces an error to be propagated up if any of the async tasks throw exceptions + return_exceptions=False, + ) + + for component in uninitialized_components: + self.initialized_components.add(component) + except Exception as e: + logger.error("Encountered issue initializing components", e) + raise e + + self.initialized_event.set() + self.status = ComponentStatus.initialized + + await self.initialize() + + # User Interfaces below + async def execute(self, input: INPUT_TYPE, **kwargs) -> OUTPUT_TYPE: + """ + The actual meat of the component. + Custom component has to implement + + If your component has to complex input data structure, make sure to override this method in order to + construct your input data with upstream components' output data + + upstream_outputs contains data that was parsed by upstreams + """ + raise NotImplementedError( + "execute function needs to be implemented by component", + ) + + @cached_property + def manifest_feature_names(self) -> Set[str]: + """ + This function defines which features are required for this component to work + + Our system will automatically fetch the required features from the feature store + to make this model evaluation possible + """ + return set() + + def get_feature( + self, + identifier: Identifier, + feature_name: str, + ) -> WyvernFeature: + """ + This function gets the feature value for the given identifier + The features are cached once fetched/evaluated. + + The feature that lives in the feature store should be + just using the feature name without the "feature_view:" prefix + For example, if your you have a feature view "fv" and a feature "wyvern_feature", + then you would have defined "fv:wyvern_feature" in manifest_feature_names. + However, when you fetch the feature value with this function, + you just have to pass in feature_name="wyvern_feature". + """ + current_request = request_context.ensure_current_request() + feature_data = current_request.feature_map.feature_map.get(identifier) + if not feature_data: + return None + return feature_data.features.get(feature_name) + + def get_all_features( + self, + identifier: Identifier, + ) -> Dict[str, WyvernFeature]: + """ + This function gets all features for the given identifier + The features are cached once fetched/evaluated. + """ + current_request = request_context.ensure_current_request() + feature_data = current_request.feature_map.feature_map.get(identifier) + if not feature_data: + return {} + return feature_data.features diff --git a/wyvern/components/events/__init__.py b/wyvern/components/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/events/events.py b/wyvern/components/events/events.py new file mode 100644 index 0000000..d3a21a7 --- /dev/null +++ b/wyvern/components/events/events.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from enum import Enum +from typing import Generic, Optional, TypeVar + +from pydantic import BaseModel +from pydantic.generics import GenericModel + +EVENT_DATA = TypeVar("EVENT_DATA", bound=BaseModel) + + +class EventType(str, Enum): + BUSINESS_LOGIC = "BUSINESS_LOGIC" + CANDIDATE = "CANDIDATE" + FEATURE = "FEATURE" + MODEL = "MODEL" + IMPRESSION = "IMPRESSION" + CUSTOM = "CUSTOM" + + +class LoggedEvent(GenericModel, Generic[EVENT_DATA]): + request_id: Optional[str] + api_source: Optional[str] + event_timestamp: Optional[datetime] + event_type: EventType + event_data: EVENT_DATA + + +class EntityEventData(BaseModel): + entity_identifier: str + entity_identifier_type: str + + +ENTITY_EVENT_DATA_TYPE = TypeVar("ENTITY_EVENT_DATA_TYPE", bound=EntityEventData) + + +class CustomEvent(LoggedEvent[ENTITY_EVENT_DATA_TYPE]): + event_type: EventType = EventType.CUSTOM diff --git a/wyvern/components/features/__init__.py b/wyvern/components/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/features/feature_logger.py b/wyvern/components/features/feature_logger.py new file mode 100644 index 0000000..9833923 --- /dev/null +++ b/wyvern/components/features/feature_logger.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from typing import Generic + +from pydantic.generics import GenericModel +from pydantic.main import BaseModel + +from wyvern import request_context +from wyvern.components.component import Component +from wyvern.components.events.events import EventType, LoggedEvent +from wyvern.entities.feature_entities import FeatureMap +from wyvern.event_logging import event_logger +from wyvern.wyvern_typing import REQUEST_ENTITY, WyvernFeature + + +class FeatureLogEventData(BaseModel): + feature_identifier: str + feature_identifier_type: str + feature_name: str + feature_value: WyvernFeature + + +class FeatureEvent(LoggedEvent[FeatureLogEventData]): + event_type: EventType = EventType.FEATURE + + +class FeatureEventLoggingRequest( + GenericModel, + Generic[REQUEST_ENTITY], +): + request: REQUEST_ENTITY + feature_map: FeatureMap + + +class FeatureEventLoggingComponent( + Component[FeatureEventLoggingRequest[REQUEST_ENTITY], None], + Generic[REQUEST_ENTITY], +): + async def execute( + self, input: FeatureEventLoggingRequest[REQUEST_ENTITY], **kwargs + ) -> None: + url_path = request_context.ensure_current_request().url_path + + def feature_event_generator(): + timestamp = datetime.utcnow() + return [ + FeatureEvent( + request_id=input.request.request_id, + api_source=url_path, + event_timestamp=timestamp, + event_data=FeatureLogEventData( + feature_identifier=feature_data.identifier.identifier, + feature_identifier_type=feature_data.identifier.identifier_type, + feature_name=feature_name, + feature_value=feature_value, + ), + ) + for feature_data in input.feature_map.feature_map.values() + for feature_name, feature_value in feature_data.features.items() + ] + + event_logger.log_events(feature_event_generator) # type: ignore diff --git a/wyvern/components/features/feature_retrieval_pipeline.py b/wyvern/components/features/feature_retrieval_pipeline.py new file mode 100644 index 0000000..9421961 --- /dev/null +++ b/wyvern/components/features/feature_retrieval_pipeline.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +import asyncio +import logging +from time import time +from typing import Generic, List, Optional, Set, Type + +from ddtrace import tracer +from pydantic.generics import GenericModel + +from wyvern.components.component import Component +from wyvern.components.features.feature_logger import ( + FeatureEventLoggingComponent, + FeatureEventLoggingRequest, +) +from wyvern.components.features.feature_store import ( + FeatureStoreRetrievalComponent, + FeatureStoreRetrievalRequest, + feature_store_retrieval_component, +) +from wyvern.components.features.realtime_features_component import ( + RealtimeFeatureComponent, + RealtimeFeatureRequest, +) +from wyvern.entities.candidate_entities import CandidateSetEntity +from wyvern.entities.feature_entities import FeatureData, FeatureMap +from wyvern.entities.feature_entity_helpers import feature_map_create, feature_map_join +from wyvern.entities.identifier_entities import WyvernEntity +from wyvern.wyvern_typing import REQUEST_ENTITY + +logger = logging.getLogger(__name__) + + +class FeatureRetrievalPipelineRequest(GenericModel, Generic[REQUEST_ENTITY]): + """ + request_feature_names includes the feature views that are needed to compute the features + + ie: product_fv:FEATURE_PRODUCT_AMOUNT_PAID_LAST_15_DAYS + """ + + request: REQUEST_ENTITY + requested_feature_names: Set[str] + feature_overrides: Set[Type[RealtimeFeatureComponent]] = set() + + +class FeatureRetrievalPipeline( + Component[FeatureRetrievalPipelineRequest[REQUEST_ENTITY], FeatureMap], + Generic[REQUEST_ENTITY], +): + def __init__( + self, + *upstreams: Component, + name: str, + ): + self.real_time_features: List[ + RealtimeFeatureComponent + ] = RealtimeFeatureComponent.real_time_features + logger.info(self.real_time_features) + self.feature_retrieval_component: FeatureStoreRetrievalComponent = ( + feature_store_retrieval_component + ) + self.feature_logger_component: FeatureEventLoggingComponent = ( + FeatureEventLoggingComponent() + ) + super().__init__( + feature_store_retrieval_component, + self.feature_logger_component, + *self.real_time_features, + *upstreams, + name=name, + ) + + @tracer.wrap(name="FeatureRetrievalPipeline._generate_real_time_features") + def _generate_real_time_features( + self, + input: FeatureRetrievalPipelineRequest[REQUEST_ENTITY], + ) -> List[RealtimeFeatureComponent]: + """ + Flatten all the real-time feature names into one list for the request/input + """ + return [ + real_time_feature + for real_time_feature in self.real_time_features + if type(real_time_feature) in input.feature_overrides + or real_time_feature.output_feature_names.intersection( + input.requested_feature_names, + ) + ] + + @tracer.wrap(name="FeatureRetrievalPipeline.execute") + async def execute( + self, input: FeatureRetrievalPipelineRequest[REQUEST_ENTITY], **kwargs + ) -> FeatureMap: + time_start = time() + + # Only evaluate real-time features where the output feature names are in the requested feature names + # Or the client wants to evaluate the feature + # TODO (suchintan): We don't support "chained" real-time features yet.. hopefully soon + real_time_features = self._generate_real_time_features(input) + + # Figure out which features are real-time features based on the definitions within the real-time feature object + features_requested_by_real_time_features = { + feature_name + for real_time_feature in real_time_features + for feature_name in real_time_feature.output_feature_names + } + + # Figure out which features come from the feature store + feature_names_to_retrieve_from_feature_store = ( + input.requested_feature_names.difference( + features_requested_by_real_time_features, + ) + ) + + all_entities = input.request.get_all_entities(cached=True) + all_identifiers = input.request.get_all_identifiers(cached=True) + # TODO (suchintan): Pass in the feature retrieval features here so they can leverage them + feature_retrieval_request = FeatureStoreRetrievalRequest( + identifiers=all_identifiers, + feature_names=list(feature_names_to_retrieve_from_feature_store), + ) + + feature_retrieval_response: FeatureMap = ( + await self.feature_retrieval_component.execute( + input=feature_retrieval_request, **kwargs + ) + ) + + # TODO (suchintan): Remove all timer related code + logger.info(f"operation feature_retrieval took:{time()-time_start:2.4f} sec") + time_start = time() + + """ + TODO (suchintan): + 1. Figure out a set of: (Candidate entities), (Non-candidate entities), (Request) + 2. Evaluate real-time features for each of the above + 3. Find the cross-section of all candidate entities and non-candidate entities + 4. Evaluate candidate features for the cross-section + 5. Combine all of the feature results together into one FeatureMap + """ + + # TODO (suchintan): Improve performance by using iterutils instead of list comprehensions + # TODO (suchintan): Figure out a better pattern for CompositeIdentifierType + # it should enforce order automatically instead of blowing up + with tracer.trace("FeatureRetrievalPipeline.generate_entities"): + candidate_entities: List[WyvernEntity] = [] + request_entities = all_entities + if isinstance(input.request, CandidateSetEntity): + """ + This branch partitions the `all_entities` list into two lists: + 1. Candidate_entities (ie Product and Brand) -- the things we will be ranking + 2. Request_entities (ie User and Query) -- + The things that are the same across all items being ranked + """ + + # TODO (shu): assume two products have the same brand: + # here we will have duplicated brands. Is it okay to have duplicated brands in candidate_entities? + # might need to dedupe + candidate_entities = [ + entity + for candidate in input.request.candidates + for entity in candidate.get_all_entities() + ] + + candidate_identifiers = { + candidate.identifier for candidate in candidate_entities + } + request_entities = [ + entity + for entity in all_entities + if entity.identifier not in candidate_identifiers + ] + + with tracer.trace("FeatureRetrievalPipeline.real_time_no_entity_features"): + request = RealtimeFeatureRequest[REQUEST_ENTITY]( + request=input.request, + feature_retrieval_response=feature_retrieval_response, + ) + real_time_request_no_entity_features: List[ + Optional[FeatureData] + ] = await asyncio.gather( + *[ + real_time_feature.compute_request_features_wrapper( + request=request, + ) + for real_time_feature in real_time_features + if real_time_feature.can_execute_on(request.request, None, None) + ] + ) + + with tracer.trace("FeatureRetrievalPipeline.real_time_entity_features"): + real_time_request_features: List[ + Optional[FeatureData] + ] = await asyncio.gather( + *[ + real_time_feature.compute_features_wrapper( + request=request, + entity=entity, + ) + for real_time_feature in real_time_features + for entity in request_entities + if real_time_feature.can_execute_on(request.request, entity, None) + ] + ) + + with tracer.trace("FeatureRetrievalPipeline.real_time_combination_features"): + real_time_request_combination_features: List[ + Optional[FeatureData] + ] = await asyncio.gather( + *[ + real_time_feature.compute_composite_features_wrapper( + primary_entity=entity, + secondary_entity=secondary_entity, + request=request, + ) + for real_time_feature in real_time_features + for entity in request_entities + for secondary_entity in request_entities + if entity != secondary_entity + and real_time_feature.can_execute_on( + request.request, + entity, + secondary_entity, + ) + ] + ) + + real_time_candidate_features: List[Optional[FeatureData]] = [] + real_time_candidate_combination_features: List[Optional[FeatureData]] = [] + + if isinstance(input.request, CandidateSetEntity): + with tracer.trace("FeatureRetrievalPipeline.real_time_candidate_features"): + real_time_candidate_features = await asyncio.gather( + *[ + real_time_feature.compute_features_wrapper( + entity=entity, + request=request, + ) + for real_time_feature in real_time_features + for entity in candidate_entities + if real_time_feature.can_execute_on( + request.request, + entity, + None, + ) + ] + ) + + with tracer.trace( + "FeatureRetrievalPipeline.real_time_candidate_combination_features", + ): + real_time_candidate_combination_features = await asyncio.gather( + *[ + real_time_feature.compute_composite_features_wrapper( + primary_entity=entity, + secondary_entity=secondary_entity, + request=request, + ) + for real_time_feature in real_time_features + for entity in candidate_entities + for secondary_entity in request_entities + if real_time_feature.can_execute_on( + request.request, + entity, + secondary_entity, + ) + ] + ) + + # TODO (kerem): Group feature views together at execution time so there never is a chance of collision + # Idea 1: No two feature views can have the feature definitions + # Idea 2: Define feature views that have the same interface, + # and we collect them together ahead of this dict comprehension + # pytest / linter validation: we should assert for feature name conflicts -- this should never happen + with tracer.trace("FeatureRetrievalPipeline.create_feature_map"): + real_time_feature_responses = feature_map_create( + *real_time_request_no_entity_features, + *real_time_request_features, + *real_time_request_combination_features, + *real_time_candidate_features, + *real_time_candidate_combination_features, + ) + + with tracer.trace("FeatureRetrievalPipeline.create_feature_response"): + await self.feature_logger_component.execute( + FeatureEventLoggingRequest( + request=input.request, + feature_map=real_time_feature_responses, + ), + ) + + # TODO (suchintan): Improve performance of this + feature_responses = feature_map_join( + feature_retrieval_response, + real_time_feature_responses, + ) + + return feature_responses diff --git a/wyvern/components/features/feature_store.py b/wyvern/components/features/feature_store.py new file mode 100644 index 0000000..6b83f57 --- /dev/null +++ b/wyvern/components/features/feature_store.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +import logging +from typing import Dict, List, Optional + +from ddtrace import tracer +from pydantic import BaseModel + +from wyvern.components.component import Component +from wyvern.config import settings +from wyvern.core.httpx import httpx_client +from wyvern.entities.feature_entities import FeatureData, FeatureMap +from wyvern.entities.identifier import Identifier +from wyvern.exceptions import WyvernFeatureNameError, WyvernFeatureStoreError +from wyvern.wyvern_typing import WyvernFeature + +logger = logging.getLogger(__name__) + + +class FeatureStoreRetrievalRequest(BaseModel): + identifiers: List[Identifier] + # TODO (suchintan): Feature names are currently linked to feature views.. + # we need to make that coupling more explicit + feature_names: List[str] + + +class FeatureStoreRetrievalComponent( + Component[FeatureStoreRetrievalRequest, FeatureMap], +): + def __init__( + self, + feature_store_host: Optional[str] = None, + feature_store_auth_token: Optional[str] = None, + ): + self.feature_store_host = ( + feature_store_host or settings.WYVERN_FEATURE_STORE_URL + ) + feature_store_auth_token = feature_store_auth_token or settings.WYVERN_API_KEY + self.request_headers = {"x-api-key": feature_store_auth_token} + + super().__init__() + + async def fetch_features_from_feature_store( + self, + identifiers: List[Identifier], + feature_names: List[str], + ) -> FeatureMap: + if not feature_names: + return FeatureMap(feature_map={}) + + logger.info(f"Fetching features from feature store: {feature_names}") + invalid_feature_names: List[str] = [ + feature_name for feature_name in feature_names if ":" not in feature_name + ] + if invalid_feature_names: + raise WyvernFeatureNameError(invalid_feature_names=invalid_feature_names) + request_body = { + "features": feature_names, + "entities": { + "IDENTIFIER": [identifier.identifier for identifier in identifiers], + }, + "full_feature_names": True, + } + # TODO (suchintan) -- chunk + parallelize this + # TODO (Suchintan): This is currently busted in local development + response = await httpx_client().post( + f"{self.feature_store_host}{settings.WYVERN_ONLINE_FEATURES_PATH}", + headers=self.request_headers, + json=request_body, + ) + + if response.status_code != 200: + logger.error(f"Error fetching features from feature store: {response}") + raise WyvernFeatureStoreError(error=response.json()) + + # TODO (suchintan): More graceful response handling here + + response_json = response.json() + feature_names = response_json["metadata"]["feature_names"] + feature_name_keys = [ + feature_name.replace("__", ":", 1) for feature_name in feature_names + ] + + results = response_json["results"] + response_identifiers = results[0]["values"] + + identifier_by_identifiers = { + identifier.identifier: identifier for identifier in identifiers + } + + feature_map: Dict[Identifier, FeatureData] = {} + for i in range(len(response_identifiers)): + feature_data: Dict[str, WyvernFeature] = { + feature_name_keys[j]: results[j]["values"][i] + # the first feature_name is IDENTIFIER which we will skip + for j in range(1, len(feature_names)) + } + + identifier = identifier_by_identifiers[response_identifiers[i]] + feature_map[identifier] = FeatureData( + identifier=identifier, + features=feature_data, + ) + + logger.info(f"Joined feature maps: {feature_map}") + return FeatureMap(feature_map=feature_map) + + @tracer.wrap(name="FeatureStoreRetrievalComponent.execute") + async def execute( + self, input: FeatureStoreRetrievalRequest, **kwargs + ) -> FeatureMap: + # TODO (suchintan): Integrate this with Feature Store + + response = await self.fetch_features_from_feature_store( + input.identifiers, + input.feature_names, + ) + return response + + +# TODO (suchintan): IS this the right way to define a singleton? +feature_store_retrieval_component = FeatureStoreRetrievalComponent() diff --git a/wyvern/components/features/realtime_features_component.py b/wyvern/components/features/realtime_features_component.py new file mode 100644 index 0000000..8e5a7f2 --- /dev/null +++ b/wyvern/components/features/realtime_features_component.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + get_args, +) + +from pydantic.generics import GenericModel + +from wyvern.components.component import Component +from wyvern.entities.feature_entities import FeatureData, FeatureMap +from wyvern.entities.identifier_entities import WyvernEntity +from wyvern.feature_store.constants import ( + FULL_FEATURE_NAME_SEPARATOR, + SQL_COLUMN_SEPARATOR, +) +from wyvern.wyvern_typing import REQUEST_ENTITY + +logger = logging.getLogger(__name__) + +PRIMARY_ENTITY = TypeVar("PRIMARY_ENTITY", bound=WyvernEntity) +SECONDARY_ENTITY = TypeVar("SECONDARY_ENTITY", bound=WyvernEntity) + + +class RealtimeFeatureRequest(GenericModel, Generic[REQUEST_ENTITY]): + request: REQUEST_ENTITY + feature_retrieval_response: FeatureMap + + +class RealtimeFeatureEntity(GenericModel, Generic[PRIMARY_ENTITY, SECONDARY_ENTITY]): + # Can pass in multiple WYVERN_ENTITIES here as well + primary_entity: Optional[PRIMARY_ENTITY] + secondary_entity: Optional[SECONDARY_ENTITY] + + +class RealtimeFeatureComponent( + Component[ + Tuple[ + RealtimeFeatureRequest[REQUEST_ENTITY], + RealtimeFeatureEntity[PRIMARY_ENTITY, SECONDARY_ENTITY], + ], + Optional[FeatureData], + ], + Generic[PRIMARY_ENTITY, SECONDARY_ENTITY, REQUEST_ENTITY], +): + NAME: str = "" + + real_time_features: List[RealtimeFeatureComponent] = [] + component_registry: Dict[str, RealtimeFeatureComponent] = {} + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + """ + This is a great spot to define a registry for RealtimeFeatureComponent + + Problem: This doesn't handle upstream relationships at all, + as the current upstream design requires a concrete instance + + Solution: Migrate the entire Component system to a registry pattern and overwrite the __new__ to return + the instance from the registry + + Caveat: Upstream relationships, circular dependencies, + and so on will have to be carefully managed with this registry + """ + instance = cls() + cls.real_time_features.append(instance) + cls.component_registry[instance.name] = instance + + def __init__( + self, + *upstreams: Component, + # TODO (suchintan) -> Refactor: Change to Type[Component] + # Then fetch component from registry during initialize instead of passing it in during __init__ + # TODO (suchintan): Eventually stop requiring these parameters + output_feature_names: Optional[Set[str]] = None, + required_feature_names: Optional[Set[str]] = None, + name: Optional[str] = None, + ): + """ + :param name: Name of the component + :param output_feature_names: features outputted by this real-time feature + """ + name = name or self.NAME + super().__init__(*upstreams, name=name) + + # TODO (suchintan): We should try to take advantage of + # https://stackoverflow.com/questions/73746553/access-type-argument-in-any-specific-subclass-of-user-defined-generict-class + # To understand what the underlying generic types really are + self.primary_entity_type = self.get_type_args_simple(0) + self.secondary_entity_type = self.get_type_args_simple(1) + self.request_entity_type = self.get_type_args_simple(2) + + # TODO shu: refactor this tuple into using the entity_identifier_type instead of the class name + self.entity_names: List[str] = [] + if self.primary_entity_type is not Any: + self.entity_names.append(self.primary_entity_type.__name__.lower()) + if self.secondary_entity_type is not Any: + self.entity_names.append(self.secondary_entity_type.__name__.lower()) + if len(self.entity_names) == 0: + if self.request_entity_type is Any: + raise ValueError( + f"RealtimeFeatureComponent {self.name} must have at least one entity type specified", + ) + self.entity_names.append(self.request_entity_type.__name__.lower()) + + self.entity_identifier_type = FULL_FEATURE_NAME_SEPARATOR.join( + self.entity_names, + ) + self.entity_identifier_type_column = SQL_COLUMN_SEPARATOR.join( + self.entity_names, + ) + + self.required_feature_names: set[str] = required_feature_names or set() + output_feature_names = output_feature_names or set() + self.output_feature_names: set[str] = { + f"{self.name}:{feature}" for feature in output_feature_names + } + + @classmethod + def get_type_args_simple(cls, index: int) -> Type: + return get_args(cls.__orig_bases__[0])[index] # type: ignore + + @classmethod + def get_entity_names( + cls, + full_feature_name: str, + ) -> Optional[List[str]]: + """ + Get the the entity identifier type, which will be used as sql column name + + full_feature_name is of the form : + """ + split_feature = full_feature_name.split(FULL_FEATURE_NAME_SEPARATOR) + if len(split_feature) != 2: + logger.warning( + f"Invalid feature name {full_feature_name} - more than one separator found", + ) + return None + + component_name, _ = split_feature + component = cls.component_registry.get(component_name) + return component.entity_names if component else None + + @classmethod + def get_entity_type_column( + cls, + full_feature_name: str, + ) -> Optional[str]: + """ + Get the the entity identifier type, which will be used as sql column name + + full_feature_name is of the form : + """ + split_feature = full_feature_name.split(FULL_FEATURE_NAME_SEPARATOR) + if len(split_feature) != 2: + logger.warning( + f"Invalid feature name {full_feature_name} - more than one separator found", + ) + return None + + component_name, _ = split_feature + component = cls.component_registry.get(component_name) + return component.entity_identifier_type_column if component else None + + def can_execute_on( + self, + request: REQUEST_ENTITY, + primary_entity: Optional[PRIMARY_ENTITY], + secondary_entity: Optional[SECONDARY_ENTITY], + ) -> bool: + """ + Checks if the input matches the entity type so we can execute on it + """ + type_matches = True + if self.primary_entity_type is not Any: + type_matches = type_matches and isinstance( + primary_entity, + self.primary_entity_type, + ) + elif primary_entity is not None: + # primary entity should be none if primary_entity_type is Any + type_matches = False + + if self.request_entity_type is not Any: + type_matches = type_matches and isinstance( + request, + self.request_entity_type, + ) + + if self.secondary_entity_type is not Any: + type_matches = type_matches and isinstance( + secondary_entity, + self.secondary_entity_type, + ) + elif secondary_entity is not None: + # secondary entity should be none if secondary_entity_type is Any + type_matches = False + + return type_matches + + async def execute( + self, + input: Tuple[ + RealtimeFeatureRequest[REQUEST_ENTITY], + RealtimeFeatureEntity[PRIMARY_ENTITY, SECONDARY_ENTITY], + ], + **kwargs, + ) -> Optional[FeatureData]: + # TODO (Suchintan): Delete this method -- this has been fully delegated upwards? + request = input[0] + entities = input[1] + + if not self.can_execute_on( + request.request, + entities.primary_entity, + entities.secondary_entity, + ): + return None + + if ( + entities.secondary_entity is not None + and entities.primary_entity is not None + ): + resp = await self.compute_composite_features( + entities.primary_entity, + entities.secondary_entity, + request, + ) + # TODO (suchintan): using this for debugging, remove later + if resp is None: + logger.info( + f"Failed to compute composite features for " + f"{self} {entities.primary_entity.identifier} {entities.secondary_entity.identifier}", + ) + return resp + + if entities.primary_entity is not None: + resp = await self.compute_features( + entities.primary_entity, + request, + ) + # TODO (suchintan): using this for debugging, remove later + if resp is None: + logger.info( + f"Failed to compute features for " + f"{self} {entities.primary_entity.identifier}", + ) + return resp + + # TODO (suchintan): Lowercase feature names? + resp = await self.compute_request_features(request) + + # TODO (suchintan): using this for debugging, remove later + if resp is None: + logger.info( + f"Failed to compute request features for {self} {request.request}", + ) + return resp + + async def compute_request_features( + self, + request: RealtimeFeatureRequest[REQUEST_ENTITY], + ) -> Optional[FeatureData]: + return None + + async def compute_features( + self, + entity: PRIMARY_ENTITY, + request: RealtimeFeatureRequest[REQUEST_ENTITY], + ) -> Optional[FeatureData]: + return None + + async def compute_composite_features( + self, + primary_entity: PRIMARY_ENTITY, + secondary_entity: SECONDARY_ENTITY, + request: RealtimeFeatureRequest[REQUEST_ENTITY], + ) -> Optional[FeatureData]: + return None + + async def compute_request_features_wrapper( + self, + request: RealtimeFeatureRequest[REQUEST_ENTITY], + ) -> Optional[FeatureData]: + feature_data = await self.compute_request_features(request) + return self.set_full_feature_name(feature_data) + + async def compute_features_wrapper( + self, + entity: PRIMARY_ENTITY, + request: RealtimeFeatureRequest[REQUEST_ENTITY], + ) -> Optional[FeatureData]: + feature_data = await self.compute_features(entity, request) + return self.set_full_feature_name(feature_data) + + async def compute_composite_features_wrapper( + self, + primary_entity: PRIMARY_ENTITY, + secondary_entity: SECONDARY_ENTITY, + request: RealtimeFeatureRequest[REQUEST_ENTITY], + ) -> Optional[FeatureData]: + feature_data = await self.compute_composite_features( + primary_entity, + secondary_entity, + request, + ) + return self.set_full_feature_name(feature_data) + + def set_full_feature_name( + self, + feature_data: Optional[FeatureData], + ) -> Optional[FeatureData]: + """ + Sets the full feature name for the feature data + """ + if not feature_data: + return None + + return FeatureData( + identifier=feature_data.identifier, + features={ + f"{self.name}:{feature_name}": feature_value + for feature_name, feature_value in feature_data.features.items() + }, + ) diff --git a/wyvern/components/helpers/__init__.py b/wyvern/components/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/helpers/linear_algebra.py b/wyvern/components/helpers/linear_algebra.py new file mode 100644 index 0000000..6f70c2f --- /dev/null +++ b/wyvern/components/helpers/linear_algebra.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import asyncio +from typing import List, Tuple + +from scipy.spatial.distance import cosine + +from wyvern.components.component import Component + + +class CosineSimilarityComponent( + Component[List[Tuple[List[float], List[float]]], List[float]], +): + def __init__(self, name: str): + super().__init__(name=name) + + async def execute( + self, + input: List[Tuple[List[float], List[float]]], + **kwargs, + ) -> List[float]: + tasks = await asyncio.gather( + *[ + self.cosine_similarity(embedding1, embedding2) + for (embedding1, embedding2) in input + ], + return_exceptions=False, + ) + # TODO (suchintan): Handle exceptions in cosine similarity function + return list(tasks) + + async def cosine_similarity( + self, + embedding_1: List[float], + embedding_2: List[float], + ) -> float: + return 1 - cosine(embedding_1, embedding_2) diff --git a/wyvern/components/helpers/sorting.py b/wyvern/components/helpers/sorting.py new file mode 100644 index 0000000..a11242f --- /dev/null +++ b/wyvern/components/helpers/sorting.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from typing import List + +from wyvern.components.component import Component +from wyvern.entities.candidate_entities import ( + GENERALIZED_WYVERN_ENTITY, + ScoredCandidate, +) + + +class SortingComponent( + Component[ + List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]], + List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]], + ], +): + def __init__(self, name: str): + super().__init__(name=name) + + async def execute( + self, + input: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]], + descending=True, + **kwargs + ) -> List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]]: + return sorted(input, key=lambda candidate: candidate.score, reverse=descending) diff --git a/wyvern/components/impressions/__init__.py b/wyvern/components/impressions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/impressions/impression_logger.py b/wyvern/components/impressions/impression_logger.py new file mode 100644 index 0000000..42b9105 --- /dev/null +++ b/wyvern/components/impressions/impression_logger.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from typing import Generic, List + +from ddtrace import tracer +from pydantic.generics import GenericModel + +from wyvern import request_context +from wyvern.components.component import Component +from wyvern.components.events.events import EntityEventData, EventType, LoggedEvent +from wyvern.entities.candidate_entities import ( + GENERALIZED_WYVERN_ENTITY, + ScoredCandidate, +) +from wyvern.event_logging import event_logger +from wyvern.wyvern_typing import REQUEST_ENTITY + + +class ImpressionEventData(EntityEventData): + impression_score: float + impression_order: int + + +class ImpressionEvent(LoggedEvent[ImpressionEventData]): + event_type: EventType = EventType.IMPRESSION + + +class ImpressionEventLoggingRequest( + GenericModel, + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + request: REQUEST_ENTITY + scored_impressions: List[ScoredCandidate[GENERALIZED_WYVERN_ENTITY]] + + +class ImpressionEventLoggingComponent( + Component[ + ImpressionEventLoggingRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + None, + ], + Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], +): + @tracer.wrap(name="ImpressionEventLoggingComponent.execute") + async def execute( + self, + input: ImpressionEventLoggingRequest[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY], + **kwargs + ) -> None: + current_span = tracer.current_span() + if current_span: + current_span.set_tag("impression_size", len(input.scored_impressions)) + url_path = request_context.ensure_current_request().url_path + + def impression_events_generator() -> List[ImpressionEvent]: + timestamp = datetime.utcnow() + impression_events = [ + ImpressionEvent( + request_id=input.request.request_id, + api_source=url_path, + event_timestamp=timestamp, + event_data=ImpressionEventData( + entity_identifier=impression.entity.identifier.identifier, + entity_identifier_type=impression.entity.identifier.identifier_type, + impression_score=impression.score, + impression_order=i, + ), + ) + for i, impression in enumerate(input.scored_impressions) + ] + return impression_events + + event_logger.log_events(impression_events_generator) # type: ignore diff --git a/wyvern/components/index/__init__.py b/wyvern/components/index/__init__.py new file mode 100644 index 0000000..99b4d12 --- /dev/null +++ b/wyvern/components/index/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from ._index import IndexDeleteComponent, IndexGetComponent, IndexUploadComponent + +__all__ = [ + "IndexDeleteComponent", + "IndexGetComponent", + "IndexUploadComponent", +] diff --git a/wyvern/components/index/_index.py b/wyvern/components/index/_index.py new file mode 100644 index 0000000..a134d14 --- /dev/null +++ b/wyvern/components/index/_index.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Type + +from wyvern.components.api_route_component import APIRouteComponent +from wyvern.entities.index_entities import ( + DeleteEntitiesRequest, + DeleteEntitiesResponse, + EntitiesRequest, + GetEntitiesResponse, + IndexRequest, + IndexResponse, +) +from wyvern.exceptions import WyvernEntityValidationError, WyvernError +from wyvern.index import WyvernEntityIndex, WyvernIndex +from wyvern.redis import wyvern_redis + +logger = logging.getLogger(__name__) + + +class IndexUploadComponent( + APIRouteComponent[IndexRequest, IndexResponse], +): + PATH: str = "/entities/upload" + REQUEST_SCHEMA_CLASS: Type[IndexRequest] = IndexRequest + RESPONSE_SCHEMA_CLASS: Type[IndexResponse] = IndexResponse + + async def execute( + self, + input: IndexRequest, + **kwargs, + ) -> IndexResponse: + """ + bulk index entities with redis pipeline + """ + + entity_internal_key = f"{input.entity_type}_id" + entity_key: str = input.entity_key or entity_internal_key + + entities: List[Dict[str, Any]] = [] + for entity in input.entities: + # validation: entity must have entity_key + if entity_key not in entity: + raise WyvernEntityValidationError( + entity_key=entity_key, + entity=entity, + ) + + if entity_internal_key not in entity: + entity[entity_internal_key] = entity[entity_key] + elif (entity_internal_key in entity) and ( + entity[entity_internal_key] != entity[entity_key] + ): + logger.warning( + f"entity already has an internal key={entity_internal_key} " + f"with value={entity[entity_internal_key]}, " + f"skipping setting the value to {entity[entity_key]}", + ) + + entities.append(entity) + + entity_ids = await wyvern_redis.bulk_index( + entities, + entity_key, + input.entity_type, + ) + + return IndexResponse( + entity_type=input.entity_type, + entity_ids=entity_ids, + ) + + +class IndexDeleteComponent( + APIRouteComponent[DeleteEntitiesRequest, DeleteEntitiesResponse], +): + PATH: str = "/entities/delete" + REQUEST_SCHEMA_CLASS: Type[DeleteEntitiesRequest] = DeleteEntitiesRequest + RESPONSE_SCHEMA_CLASS: Type[DeleteEntitiesResponse] = DeleteEntitiesResponse + + async def execute( + self, + input: DeleteEntitiesRequest, + **kwargs, + ) -> DeleteEntitiesResponse: + await WyvernIndex.bulk_delete(input.entity_type, input.entity_ids) + return DeleteEntitiesResponse( + entity_ids=input.entity_ids, + entity_type=input.entity_type, + ) + + +class IndexGetComponent( + APIRouteComponent[EntitiesRequest, GetEntitiesResponse], +): + PATH: str = "/entities/get" + REQUEST_SCHEMA_CLASS: Type[EntitiesRequest] = EntitiesRequest + RESPONSE_SCHEMA_CLASS: Type[GetEntitiesResponse] = GetEntitiesResponse + + async def execute( + self, + input: EntitiesRequest, + **kwargs, + ) -> GetEntitiesResponse: + entities = await WyvernEntityIndex.bulk_get( + entity_type=input.entity_type, + entity_ids=input.entity_ids, + ) + if len(entities) != len(input.entity_ids): + raise WyvernError("Unexpected Error") + entity_map = {input.entity_ids[i]: entities[i] for i in range(len(entities))} + return GetEntitiesResponse( + entity_type=input.entity_type, + entities=entity_map, + ) diff --git a/wyvern/components/models/__init__.py b/wyvern/components/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/models/model_component.py b/wyvern/components/models/model_component.py new file mode 100644 index 0000000..248977d --- /dev/null +++ b/wyvern/components/models/model_component.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +import logging +from datetime import datetime +from functools import cached_property +from typing import Dict, Generic, List, Optional, Set, TypeVar, Union + +from pydantic import BaseModel +from pydantic.generics import GenericModel + +from wyvern import request_context +from wyvern.components.component import Component +from wyvern.components.events.events import EventType, LoggedEvent +from wyvern.entities.identifier import Identifier +from wyvern.event_logging import event_logger +from wyvern.exceptions import WyvernModelInputError +from wyvern.wyvern_typing import GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY + +MODEL_OUTPUT_DATA_TYPE = TypeVar( + "MODEL_OUTPUT_DATA_TYPE", + bound=Union[float, str, List[float]], +) + +logger = logging.getLogger(__name__) + + +class ModelEventData(BaseModel): + model_name: str + model_output: str + entity_identifier: Optional[str] = None + entity_identifier_type: Optional[str] = None + + +class ModelEvent(LoggedEvent[ModelEventData]): + event_type: EventType = EventType.MODEL + + +class ModelOutput(GenericModel, Generic[MODEL_OUTPUT_DATA_TYPE]): + data: Dict[Identifier, Optional[MODEL_OUTPUT_DATA_TYPE]] + model_name: Optional[str] = None + + def get_entity_output( + self, + identifier: Identifier, + ) -> Optional[MODEL_OUTPUT_DATA_TYPE]: + return self.data.get(identifier) + + +class ModelInput(GenericModel, Generic[GENERALIZED_WYVERN_ENTITY, REQUEST_ENTITY]): + request: REQUEST_ENTITY + entities: List[GENERALIZED_WYVERN_ENTITY] = [] + + @property + def first_entity(self) -> GENERALIZED_WYVERN_ENTITY: + if not self.entities: + raise WyvernModelInputError(model_input=self) + return self.entities[0] + + @property + def first_identifier(self) -> Identifier: + return self.first_entity.identifier + + +MODEL_INPUT = TypeVar("MODEL_INPUT", bound=ModelInput) +MODEL_OUTPUT = TypeVar("MODEL_OUTPUT", bound=ModelOutput) + + +class ModelComponent( + Component[ + MODEL_INPUT, + MODEL_OUTPUT, + ], +): + @cached_property + def manifest_feature_names(self) -> Set[str]: + """ + This function defines which features are necessary for model evaluation + + Our system will automatically fetch the required features from the feature store + to make this model evaluation possible + """ + raise NotImplementedError( + f"{self.__class__.__name__} is a ModelComponent. " + "The @cached_property function `manifest_feature_names` must be " + "implemented to define features required for the model.", + ) + + async def execute(self, input: MODEL_INPUT, **kwargs) -> MODEL_OUTPUT: + """ + The model_name and model_score will be automatically logged + """ + api_source = request_context.ensure_current_request().url_path + request_id = input.request.request_id + model_output = await self.inference(input, **kwargs) + + def events_generator() -> List[ModelEvent]: + timestamp = datetime.utcnow() + return [ + ModelEvent( + request_id=request_id, + api_source=api_source, + event_timestamp=timestamp, + event_data=ModelEventData( + model_name=model_output.model_name or self.__class__.__name__, + model_output=str(output), + entity_identifier=identifier.identifier, + entity_identifier_type=identifier.identifier_type, + ), + ) + for identifier, output in model_output.data.items() + ] + + event_logger.log_events(events_generator) # type: ignore + + return model_output + + async def inference( + self, + input: MODEL_INPUT, + **kwargs, + ) -> MODEL_OUTPUT: + raise NotImplementedError + + +# class EmbeddingModelComponent( +# ModelComponent[List[INPUT_TYPE], List[Embeddings], MODEL_OUTPUT], +# Generic[INPUT_TYPE, MODEL_OUTPUT], +# ): +# async def execute(self, input: List[INPUT_TYPE], **kwargs) -> List[Embeddings]: +# return await self.batch_embed(inputs=input, **kwargs) + +# async def batch_embed(self, inputs: List[INPUT_TYPE], **kwargs) -> List[Embeddings]: +# """ +# Implement this function to get your model to embed a batch of inputs +# You should implement this when it's more efficient to embed a batch of inputs at once +# compared to embedding them in a loop +# """ +# tasks = [self.embed(input=input) for input in inputs] + +# task_results = await asyncio.gather(*tasks, return_exceptions=False) + +# # TODO (suchintan): Handle exceptions in the task handler +# return list(task_results) + +# @abstractmethod +# async def embed(self, input: INPUT_TYPE, **kwargs) -> Embeddings: +# """ +# Implement this function to get your model to embed a particular input +# """ +# ... diff --git a/wyvern/components/models/modelbit_component.py b/wyvern/components/models/modelbit_component.py new file mode 100644 index 0000000..bff5bfc --- /dev/null +++ b/wyvern/components/models/modelbit_component.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +import asyncio +import logging +from functools import cached_property +from typing import ( + Any, + Dict, + List, + Optional, + Set, + Tuple, + Type, + TypeAlias, + Union, + get_args, +) + +from wyvern.components.models.model_component import ( + MODEL_INPUT, + MODEL_OUTPUT, + ModelComponent, +) +from wyvern.config import settings +from wyvern.core.httpx import httpx_client +from wyvern.entities.identifier import Identifier +from wyvern.entities.identifier_entities import WyvernEntity +from wyvern.entities.request import BaseWyvernRequest +from wyvern.exceptions import ( + WyvernModelbitTokenMissingError, + WyvernModelbitValidationError, +) + +JSON: TypeAlias = Union[Dict[str, "JSON"], List["JSON"], str, int, float, bool, None] +logger = logging.getLogger(__name__) + + +class ModelbitComponent(ModelComponent[MODEL_INPUT, MODEL_OUTPUT]): + AUTH_TOKEN: str = "" + URL: str = "" + + def __init__( + self, + *upstreams, + name: Optional[str] = None, + auth_token: Optional[str] = None, + url: Optional[str] = None, + ) -> None: + super().__init__(*upstreams, name=name) + self._auth_token = auth_token or self.AUTH_TOKEN + self._modelbit_url = url or self.URL + self.headers = { + "Authorization": f"Bearer {self._auth_token}", + "Content-Type": "application/json", + } + + # TODO shu: test out the model_input_type + self.model_input_type = self.get_type_args_simple(0) + self.model_ouput_type = self.get_type_args_simple(1) + + if not self._auth_token: + raise WyvernModelbitTokenMissingError() + + @cached_property + def modelbit_features(self) -> List[str]: + return [] + + @cached_property + def manifest_feature_names(self) -> Set[str]: + return set(self.modelbit_features) + + @classmethod + def get_type_args_simple(cls, index: int) -> Type: + return get_args(cls.__orig_bases__[0])[index] # type: ignore + + async def build_requests( + self, + input: MODEL_INPUT, + ) -> Tuple[List[Identifier], List[Any]]: + """ + Please refer to modlebit batch inference API: + https://doc.modelbit.com/deployments/rest-api/ + """ + target_entities: List[ + Union[WyvernEntity, BaseWyvernRequest] + ] = input.entities or [input.request] + target_identifiers = [entity.identifier for entity in target_entities] + all_requests = [ + [ + idx + 1, + { + "features": [ + self.get_feature(identifier, feature_name) + for feature_name in self.modelbit_features + ], + }, + ] + for idx, identifier in enumerate(target_identifiers) + ] + return target_identifiers, all_requests + + async def inference(self, input: MODEL_INPUT, **kwargs) -> MODEL_OUTPUT: + # TODO shu: currently we don't support modelbit inference just for request if the input contains entities + + target_identifiers, all_requests = await self.build_requests(input) + + if len(target_identifiers) != len(all_requests): + raise WyvernModelbitValidationError( + f"Number of identifiers ({len(target_identifiers)}) " + f"does not match number of modelbit requests ({len(all_requests)})", + ) + + # split requests into smaller batches and parallelize them + futures = [ + httpx_client().post( + self._modelbit_url, + headers=self.headers, + json={"data": all_requests[i : i + settings.MODELBIT_BATCH_SIZE]}, + ) + for i in range(0, len(all_requests), settings.MODELBIT_BATCH_SIZE) + ] + responses = await asyncio.gather(*futures) + # resp_list: List[List[float]] = resp.json().get("data", []) + output_data: Dict[Identifier, Optional[Union[float, str, List[float]]]] = {} + + for batch_idx, resp in enumerate(responses): + if resp.status_code != 200: + logger.warning(f"Modelbit inference failed: {resp.text}") + continue + resp_list: List[ + List[Union[float, str, List[float], None]] + ] = resp.json().get( + "data", + [], + ) + for idx, individual_output in enumerate(resp_list): + # individual_output[0] is the index of modelbit output which is useless so we'll not use it + # individual_output[1] is the actual output + output_data[ + target_identifiers[batch_idx * settings.MODELBIT_BATCH_SIZE + idx] + ] = individual_output[1] + + return self.model_ouput_type( + data=output_data, + model_name=self.name, + ) diff --git a/wyvern/components/pagination/__init__.py b/wyvern/components/pagination/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/components/pagination/pagination_component.py b/wyvern/components/pagination/pagination_component.py new file mode 100644 index 0000000..1761743 --- /dev/null +++ b/wyvern/components/pagination/pagination_component.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +import logging +from typing import Generic, List + +from pydantic.generics import GenericModel + +from wyvern.components.component import Component +from wyvern.components.pagination.pagination_fields import PaginationFields +from wyvern.exceptions import PaginationError +from wyvern.wyvern_typing import T + +logger = logging.getLogger(__name__) + + +class PaginationRequest(GenericModel, Generic[T]): + pagination_fields: PaginationFields + entities: List[T] + + +class PaginationComponent(Component[PaginationRequest[T], List[T]]): + def __init__(self): + super().__init__(name="PaginationComponent") + + async def execute(self, input: PaginationRequest[T], **kwargs) -> List[T]: + user_page = input.pagination_fields.user_page + candidate_page = input.pagination_fields.candidate_page + candidate_page_size = input.pagination_fields.candidate_page_size + user_page_size = input.pagination_fields.user_page_size + + ranking_page = user_page - ( + candidate_page * candidate_page_size / user_page_size + ) + + start_index = int(ranking_page * user_page_size) + end_index = min(int((ranking_page + 1) * user_page_size), len(input.entities)) + + # TODO (suchintan): Add test case, this can happen if candidate page > user page + if ranking_page < 0: + message = ( + f"Ranking page {ranking_page} is less than 0. Is the user_page correct?. " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + # TODO (suchintan): I wonder if we can have this kind of validation live in the FastApi layer + # TODO (suchintan): Add test case + if candidate_page < 0 or user_page < 0: + message = ( + f"User page {user_page} or candidate page {candidate_page} is less than 0, " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + # TODO (suchintan): Add test case + if candidate_page_size > 1000 or candidate_page_size < 0: + message = ( + f"Candidate page size {candidate_page_size} is greater than 1000 or less than 0, " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + # TODO (suchintan): Add test case + if len(input.entities) > 1000: + message = ( + f"Number of entities {len(input.entities)} is greater than 1000, " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + # TODO (suchintan): Add test case + if user_page_size > 100 or user_page_size < 0: + message = ( + f"User page size {user_page_size} is greater than 100 or less than 0, " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + if user_page_size > candidate_page_size: + message = ( + f"User page size {user_page_size} is greater than candidate page size {candidate_page_size}, " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + # TODO (suchintan): Add test case + if end_index > len(input.entities): + message = ( + f"Computed End index {end_index} is greater than the number of entities {len(input.entities)}, " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + # This should NEVER happen, but add a case here + if end_index <= start_index: + message = ( + f"Computed end_index={end_index} is less than or equal to the start_index={start_index} " + f"number_of_entities={len(input.entities)}, " + f"pagination_fields={input.pagination_fields}" + ) + logger.error(message) + raise PaginationError(message) + + # TODO (suchintan): Add test case + return input.entities[start_index:end_index] diff --git a/wyvern/components/pagination/pagination_fields.py b/wyvern/components/pagination/pagination_fields.py new file mode 100644 index 0000000..fff2ec9 --- /dev/null +++ b/wyvern/components/pagination/pagination_fields.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from pydantic import BaseModel + + +class PaginationFields(BaseModel): + user_page_size: int + user_page: int + candidate_page_size: int + candidate_page: int diff --git a/wyvern/components/pipeline_component.py b/wyvern/components/pipeline_component.py new file mode 100644 index 0000000..54483ac --- /dev/null +++ b/wyvern/components/pipeline_component.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from functools import cached_property +from typing import Optional, Set, Type + +from wyvern import request_context +from wyvern.components.api_route_component import APIRouteComponent +from wyvern.components.component import Component +from wyvern.components.features.feature_retrieval_pipeline import ( + FeatureRetrievalPipeline, + FeatureRetrievalPipelineRequest, +) +from wyvern.components.features.realtime_features_component import ( + RealtimeFeatureComponent, +) +from wyvern.wyvern_typing import REQUEST_ENTITY, RESPONSE_SCHEMA + + +class PipelineComponent(APIRouteComponent[REQUEST_ENTITY, RESPONSE_SCHEMA]): + def __init__(self, *upstreams: Component, name: Optional[str] = None) -> None: + # TODO Kerem: if upstreams has the FeatureRetrievalPipeline, then the code is broken + self.feature_retrieval_pipeline = FeatureRetrievalPipeline[REQUEST_ENTITY]( + name=f"{self.__class__.__name__}-feature_retrieval", + ) + self.feature_names: Set[str] = set() + super().__init__(*upstreams, self.feature_retrieval_pipeline, name=name) + + @cached_property + def realtime_features_overrides(self) -> Set[Type[RealtimeFeatureComponent]]: + """ + This function defines the set of RealtimeFeatureComponents that generates features + with non-deterministic feature names. + For example, feature names like matched_query_brand. + That feature is defined like matched_query_{input.query.matched_query}, so it can refer to 10 or 20 features + """ + return set() + + async def initialize(self) -> None: + # get all the feature names from all the upstream components + for component in self.initialized_components: + for feature_name in component.manifest_feature_names: + self.feature_names.add(feature_name) + + async def retrieve_features(self, request: REQUEST_ENTITY) -> None: + """ + TODO shu: it doesn't support feature overrides. Write code to support that + """ + feature_request = FeatureRetrievalPipelineRequest[REQUEST_ENTITY]( + request=request, + requested_feature_names=self.feature_names, + feature_overrides=self.realtime_features_overrides, + ) + feature_map = await self.feature_retrieval_pipeline.execute( + feature_request, + ) + current_request = request_context.ensure_current_request() + current_request.feature_map = feature_map + + async def warm_up(self, input: REQUEST_ENTITY) -> None: + await super().warm_up(input) + + # TODO shu: split feature_retrieval_pipeline into + # 1. feature retrieval from feature store 2. realtime feature computation + # then the warm_up and feature retrieval from feature store can be done in parallel + # suchintan: we also need to retrieve brand features from the feature store + # and brand info would only be available via hydration so hydration has to be done first + await self.retrieve_features(input) diff --git a/wyvern/config.py b/wyvern/config.py new file mode 100644 index 0000000..225f64b --- /dev/null +++ b/wyvern/config.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from pydantic import BaseSettings + + +class Settings(BaseSettings): + ENVIRONMENT: str = "development" + + PROJECT_NAME: str = "default" + + REDIS_HOST: str = "localhost" + REDIS_PORT: int = 6379 + REDIS_SCOPE: str = "default" + + # URLs + WYVERN_BASE_URL = "https://api.wyvern.ai" + WYVERN_ONLINE_FEATURES_PATH: str = "/feature/get-online-features" + WYVERN_HISTORICAL_FEATURES_PATH: str = "/feature/get-historical-features" + WYVERN_FEATURE_STORE_URL: str = "https://api.wyvern.ai" + + WYVERN_API_KEY: str = "" + + # Snowflake configurations + SNOWFLAKE_ACCOUNT: str = "" + SNOWFLAKE_USER: str = "" + SNOWFLAKE_PASSWORD: str = "" + SNOWFLAKE_ROLE: str = "" + SNOWFLAKE_WAREHOUSE: str = "" + SNOWFLAKE_DATABASE: str = "" + SNOWFLAKE_OFFLINE_STORE_SCHEMA: str = "PUBLIC" + + # NOTE: aws configs are used for feature logging with AWS firehose + AWS_ACCESS_KEY_ID: str = "" + AWS_SECRET_ACCESS_KEY: str = "" + AWS_REGION_NAME: str = "us-east-1" + + FEATURE_STORE_TIMEOUT: int = 60 + SERVER_TIMEOUT: int = 60 + + # pipeline service configurations + REDIS_BATCH_SIZE: int = 100 + + WYVERN_INDEX_VERSION: int = 1 + + MODELBIT_BATCH_SIZE: int = 30 + + class Config: + env_file = (".env", ".env.prod") + env_file_encoding = "utf-8" + + +settings = Settings() diff --git a/wyvern/core/__init__.py b/wyvern/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/core/compression.py b/wyvern/core/compression.py new file mode 100644 index 0000000..99311ac --- /dev/null +++ b/wyvern/core/compression.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict, Union + +import lz4.frame +import msgspec + +msgspec_json_encoder = msgspec.json.Encoder() +msgspec_json_decoder = msgspec.json.Decoder() + + +def wyvern_encode(data: Dict[str, Any]) -> bytes: + """ + encode a dict to compressed bytes using lz4.frame + """ + return lz4.frame.compress(msgspec_json_encoder.encode(data)) + + +def wyvern_decode(data: Union[bytes, str]) -> Dict[str, Any]: + """ + decode compressed bytes to a dict with lz4.frame + """ + return msgspec_json_decoder.decode(lz4.frame.decompress(data)) diff --git a/wyvern/core/httpx.py b/wyvern/core/httpx.py new file mode 100644 index 0000000..d7e3db3 --- /dev/null +++ b/wyvern/core/httpx.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +import logging + +import httpx + +from wyvern.exceptions import WyvernError + +logger = logging.getLogger(__name__) +DEFAULT_HTTPX_TIMEOUT = 60 + + +class HTTPXClientWrapper: + + async_client = None + + def start(self): + """Instantiate the client. Call from the FastAPI startup hook.""" + self.async_client = httpx.AsyncClient(timeout=DEFAULT_HTTPX_TIMEOUT) + logger.info(f"httpx AsyncClient instantiated. Id {id(self.async_client)}") + + async def stop(self): + """Gracefully shutdown. Call from FastAPI shutdown hook.""" + if not self.async_client: + return + logger.info( + f"httpx async_client.is_closed(): {self.async_client.is_closed} - Now close it. " + f"Id (will be unchanged): {id(self.async_client)}", + ) + if self.async_client and not self.async_client.is_closed: + await self.async_client.aclose() + logger.info( + f"httpx async_client.is_closed(): {self.async_client.is_closed}. " + f"Id (will be unchanged): {id(self.async_client)}", + ) + self.async_client = None + logger.info("httpx AsyncClient closed") + + def __call__(self): + """Calling the instantiated HTTPXClientWrapper returns the wrapped singleton.""" + # Ensure we don't use it if not started / running + if self.async_client is None: + raise WyvernError("HTTPXClientWrapper not started") + + return self.async_client + + +httpx_client = HTTPXClientWrapper() diff --git a/wyvern/entities/__init__.py b/wyvern/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/entities/candidate_entities.py b/wyvern/entities/candidate_entities.py new file mode 100644 index 0000000..709abda --- /dev/null +++ b/wyvern/entities/candidate_entities.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import Generic, List, TypeVar + +from pydantic.generics import GenericModel + +from wyvern.entities.identifier_entities import WyvernDataModel +from wyvern.wyvern_typing import GENERALIZED_WYVERN_ENTITY + + +# TODO (suchintan): This should be renamed to ScoredEntity probably +class ScoredCandidate(GenericModel, Generic[GENERALIZED_WYVERN_ENTITY]): + entity: GENERALIZED_WYVERN_ENTITY + score: float = 0.0 + + +class CandidateSetEntity( + WyvernDataModel, + GenericModel, + Generic[GENERALIZED_WYVERN_ENTITY], +): + candidates: List[GENERALIZED_WYVERN_ENTITY] + + +CANDIDATE_SET_ENTITY = TypeVar("CANDIDATE_SET_ENTITY", bound=CandidateSetEntity) diff --git a/wyvern/entities/feature_entities.py b/wyvern/entities/feature_entities.py new file mode 100644 index 0000000..cfbf7d8 --- /dev/null +++ b/wyvern/entities/feature_entities.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import Dict + +from pydantic.main import BaseModel + +from wyvern.entities.identifier import Identifier +from wyvern.wyvern_typing import WyvernFeature + + +class FeatureData(BaseModel, frozen=True): + identifier: Identifier + features: Dict[str, WyvernFeature] = {} + + def __str__(self) -> str: + return f"identifier={self.identifier} features={self.features}" + + def __repr__(self): + return self.__str__() + + +class FeatureMap(BaseModel, frozen=True): + feature_map: Dict[Identifier, FeatureData] diff --git a/wyvern/entities/feature_entity_helpers.py b/wyvern/entities/feature_entity_helpers.py new file mode 100644 index 0000000..8fab176 --- /dev/null +++ b/wyvern/entities/feature_entity_helpers.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from typing import Dict, Optional + +from wyvern.entities.feature_entities import FeatureData, FeatureMap +from wyvern.entities.identifier import Identifier + + +def feature_map_join(*feature_maps: FeatureMap) -> FeatureMap: + return feature_map_create( + *[value for map in feature_maps for value in map.feature_map.values()] + ) + + +def feature_map_create(*feature_data: Optional[FeatureData]) -> FeatureMap: + feature_map: Dict[Identifier, FeatureData] = {} + for data in feature_data: + if data is None: + continue + + if data.identifier in feature_map: + # print(f"Duplicate keys found in feature map {data}") + # TODO (suchintan): handle duplicate keys at this stage + feature_map[data.identifier].features.update(data.features) + else: + feature_map[data.identifier] = data + + return FeatureMap(feature_map=feature_map) diff --git a/wyvern/entities/identifier.py b/wyvern/entities/identifier.py new file mode 100644 index 0000000..aff2bf6 --- /dev/null +++ b/wyvern/entities/identifier.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import logging +from enum import Enum +from typing import Union + +from pydantic.main import BaseModel + +from wyvern.config import settings +from wyvern.utils import generate_index_key + +COMPOSITE_SEPARATOR = ":" +logger = logging.getLogger(__name__) + + +class SimpleIdentifierType(str, Enum): + PRODUCT = "product" + QUERY = "query" + BRAND = "brand" + CATEGORY = "category" + USER = "user" + REQUEST = "request" + + +class CompositeIdentifierType(str, Enum): + @staticmethod + def composite( + primary_identifier_type: SimpleIdentifierType, + secondary_identifier_type: SimpleIdentifierType, + ) -> str: + return ( + f"{primary_identifier_type}{COMPOSITE_SEPARATOR}{secondary_identifier_type}" + ) + + PRODUCT_QUERY = composite( + SimpleIdentifierType.PRODUCT, + SimpleIdentifierType.QUERY, + ) + BRAND_QUERY = composite(SimpleIdentifierType.BRAND, SimpleIdentifierType.QUERY) + CATEGORY_QUERY = composite( + SimpleIdentifierType.CATEGORY, + SimpleIdentifierType.QUERY, + ) + USER_PRODUCT = composite(SimpleIdentifierType.PRODUCT, SimpleIdentifierType.USER) + USER_BRAND = composite(SimpleIdentifierType.BRAND, SimpleIdentifierType.USER) + USER_CATEGORY = composite(SimpleIdentifierType.CATEGORY, SimpleIdentifierType.USER) + QUERY_USER = composite(SimpleIdentifierType.QUERY, SimpleIdentifierType.USER) + + +IdentifierType = Union[SimpleIdentifierType, CompositeIdentifierType] + + +class Identifier(BaseModel): + """ + Identifiers exist to represent a unique entity through their unique id and their type + For example: a product with id p_1234 and type "product" or a user with id u_1234 and type "user" + + Composite identifiers are also possible, for example: + a product with id p_1234 and type "product" + a user with id u_1234 and type "user" + + The composite identifier would be "p_1234:u_1234", + and the composite identifier_type would be "product:user" + """ + + identifier: str + identifier_type: str + + class Config: + frozen = True + + def __str__(self) -> str: + return f"{self.identifier_type}::{self.identifier}" + + def __repr__(self): + return self.__str__() + + def __hash__(self): + return hash(self.__str__()) + + @staticmethod + def as_identifier_type( + identifier_type_string: str, + ) -> IdentifierType: + try: + return SimpleIdentifierType(identifier_type_string) + except ValueError: + pass + return CompositeIdentifierType(identifier_type_string) + + def index_key(self) -> str: + return generate_index_key( + settings.PROJECT_NAME, + self.identifier_type, + self.identifier, + ) + + +class CompositeIdentifier(Identifier): + primary_identifier: Identifier + secondary_identifier: Identifier + + def __init__( + self, primary_identifier: Identifier, secondary_identifier: Identifier, **kwargs + ): + identifier = f"{primary_identifier.identifier}{COMPOSITE_SEPARATOR}{secondary_identifier.identifier}" + identifier_type = self.as_identifier_type( + primary_identifier.identifier_type + + COMPOSITE_SEPARATOR + + secondary_identifier.identifier_type, + ) + super().__init__( + identifier=identifier, + identifier_type=identifier_type, + primary_identifier=primary_identifier, + secondary_identifier=secondary_identifier, + **kwargs, + ) diff --git a/wyvern/entities/identifier_entities.py b/wyvern/entities/identifier_entities.py new file mode 100644 index 0000000..b891bf4 --- /dev/null +++ b/wyvern/entities/identifier_entities.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Set + +from pydantic import BaseModel, PrivateAttr + +from wyvern.entities.identifier import Identifier, SimpleIdentifierType + + +class WyvernDataModel(BaseModel): + _all_entities: Optional[List[WyvernEntity]] = PrivateAttr() + _all_identifiers: Optional[List[Identifier]] = PrivateAttr() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # for performance purpose, we're caching all the entities and identifiers + self._all_entities = None + self._all_identifiers = None + + def index_fields(self) -> List[str]: + """ + This method returns a list of fields that contains indexable data + """ + return [] + + # TODO (suchintan): Should we turn this into a `@property`? + def get_all_entities(self, cached: bool = True) -> List[WyvernEntity]: + """ + This method returns all of the entities associated with subclasses of this + + If cached is True, all the nodes under the tree will be cached + """ + if cached and self._all_entities is not None: + return self._all_entities + + # TODO (suchintan): Autogenerate composite identifiers here if possible + # TODO (suchintan): Convert this to a property without causing issues lol + # DFS to get all identifiers so that each entity could cache its own results + all_entities: List[WyvernEntity] = [] + all_identifiers: List[Identifier] = [] + all_identifiers_set: Set[Identifier] = set() + + if isinstance(self, WyvernEntity): + all_entities.append(self) + all_identifiers.append(self.identifier) + all_identifiers_set.add(self.identifier) + + for field in self.__fields__: + value = getattr(self, field) + if isinstance(value, WyvernDataModel): + self._handle_value( + value, + all_entities, + all_identifiers, + all_identifiers_set, + cached, + ) + elif isinstance(value, list): + for item in value: + if not isinstance(item, WyvernDataModel): + continue + self._handle_value( + item, + all_entities, + all_identifiers, + all_identifiers_set, + cached, + ) + if cached: + self._all_entities = all_entities + self._all_identifiers = all_identifiers + else: + # empty the cache + self._all_entities = None + self._all_identifiers = None + return all_entities + + def _handle_value( + self, + value: WyvernDataModel, + all_entities: List[WyvernEntity], + all_identifiers: List[Identifier], + all_identifiers_set: Set[Identifier], + cached: bool, + ) -> None: + field_entities = value.get_all_entities(cached=cached) + for field_entity in field_entities: + if field_entity.identifier in all_identifiers_set: + continue + all_identifiers_set.add(field_entity.identifier) + all_identifiers.append(field_entity.identifier) + all_entities.append(field_entity) + + # TODO (suchintan): Should we turn this into a `@property`? + def get_all_identifiers(self, cached: bool = True) -> List[Identifier]: + """ + This method generally returns all of the identifiers associated with subclasses of this + + Example: You create a QueryProductEntity with query="test" and product_id="1234" + It subclasses QueryEntity and ProductEntity, which both have an identifier + This method will return a list of both of those identifiers + + Example: You create a ProductSearchRankingRequest with + query="test", candidates=["1234", ...], user="u_1234" + This method will return the user and query identifier + It will also return the identifiers for each candidate (thanks to the implementation in CandidateEntity) + + Note: While this checks for `WyvernEntity` -- a `WyvernDataModel` can have many + entities within it, it itself may not be an entity + """ + + if cached: + if self._all_identifiers is None: + self.get_all_entities(cached=cached) + return self._all_identifiers or [] + else: + return [ + entity.identifier for entity in self.get_all_entities(cached=cached) + ] + + def nested_hydration(self) -> Dict[str, str]: + """ + A dictionary that maps the entity id field name to the nested entity field name + + TODO: [SHU] replace this mapping by introducing `class WyvernField(pydantic.Field)` + to represent the "entity ide field", which will reference to the nested entity field name + """ + return {} + + +class WyvernEntity(WyvernDataModel): + """ + WyvernEntity is a base class for all entities that have primary identifier + TODO: + we want to design a way to so that + 1. the primary key of the entity could map to the name of the entity + 2. it's easy to define the relation + + example: + have a @wyvern_entity decorator that could be used to define the primary key name + and identifier type + @wyvenr_entity(key="product_id", type="product") + """ + + _identifier: Identifier = PrivateAttr() + + class Config: + validate_assignment = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._identifier = self.generate_identifier() + + @property + def identifier(self) -> Identifier: + """ + This method returns the identifier for this entity + """ + return self._identifier + + def generate_identifier(self) -> Identifier: + raise NotImplementedError + + def dict(self, *args, **kwargs): + results = super().dict(*args, **kwargs) + # enrich data + results.update({"identifier": self.identifier.dict()}) + return results + + def load_fields(self, data: Dict[str, Any]) -> None: + """ + This method load the entity with the given data. + The return data is the nested entities that need to be further hydrated + + For example: + if a Product contains these two fields: `brand_id: Optional[str]` and `brand: Optional[Brand]`, + as the hydrated entity. We fetch the brand_id for the product from Wyvern Index, + as the first hydration step for Product entity, then we fetch brand entity from Wyvern Index, + as the second hydration step + """ + for field in self.__fields__: + value = getattr(self, field) + if value: + continue + if field in data: + setattr(self, field, data[field]) + + +class QueryEntity(WyvernEntity): + query: str + + def generate_identifier(self) -> Identifier: + return Identifier( + identifier=self.query, + identifier_type=SimpleIdentifierType.QUERY, + ) + + +class ProductEntity(WyvernEntity): + product_id: str + + def generate_identifier(self) -> Identifier: + return Identifier( + identifier=self.product_id, + identifier_type=SimpleIdentifierType.PRODUCT, + ) + + +class UserEntity(WyvernEntity): + user_id: str + + def generate_identifier(self) -> Identifier: + return Identifier( + identifier=self.user_id, + identifier_type=SimpleIdentifierType.USER, + ) diff --git a/wyvern/entities/index_entities.py b/wyvern/entities/index_entities.py new file mode 100644 index 0000000..65e6245 --- /dev/null +++ b/wyvern/entities/index_entities.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + +from wyvern.entities.identifier import SimpleIdentifierType + +MIN_INDEX_ITEMS = 0 +MAX_INDEX_ITEMS = 1000 + + +class IndexResponse(BaseModel): + entity_type: str + entity_ids: List[str] + + +class IndexRequest(BaseModel): + entities: List[Dict[Any, Any]] = Field( + min_items=MIN_INDEX_ITEMS, + max_items=MAX_INDEX_ITEMS, + ) + entity_type: SimpleIdentifierType + entity_key: Optional[str] + + +class EntitiesRequest(BaseModel): + entity_ids: List[str] = Field( + min_items=MIN_INDEX_ITEMS, + max_items=MAX_INDEX_ITEMS, + ) + entity_type: SimpleIdentifierType + + +class DeleteEntitiesRequest(EntitiesRequest): + pass + + +class GetEntitiesResponse(BaseModel): + entity_type: str + entities: Dict[str, Optional[Dict[Any, Any]]] = Field(default_factory=dict) + + +class DeleteEntitiesResponse(BaseModel): + entity_type: str + entity_ids: List[str] diff --git a/wyvern/entities/request.py b/wyvern/entities/request.py new file mode 100644 index 0000000..caced1b --- /dev/null +++ b/wyvern/entities/request.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from typing import Optional + +from wyvern.entities.identifier import Identifier +from wyvern.entities.identifier_entities import WyvernDataModel + + +class BaseWyvernRequest(WyvernDataModel): + request_id: str + include_events: Optional[bool] = False + + @property + def identifier(self) -> Identifier: + return Identifier( + identifier=self.request_id, + identifier_type="request", + ) diff --git a/wyvern/event_logging/__init__.py b/wyvern/event_logging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/event_logging/event_logger.py b/wyvern/event_logging/event_logger.py new file mode 100644 index 0000000..b4fe6c6 --- /dev/null +++ b/wyvern/event_logging/event_logger.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from typing import Any, Callable, List + +from wyvern import request_context +from wyvern.components.events.events import CustomEvent, EntityEventData, LoggedEvent + + +def log_events(event_generator: Callable[[], List[LoggedEvent]]): + request_context.ensure_current_request().events.append(event_generator) + + +def get_logged_events() -> List[LoggedEvent[Any]]: + return [ + event + for event_generator in request_context.ensure_current_request().events + for event in event_generator() + ] + + +def get_logged_events_generator() -> List[Callable[[], List[LoggedEvent[Any]]]]: + return request_context.ensure_current_request().events + + +def log_custom_events(events: List[EntityEventData]) -> None: + request = request_context.ensure_current_request() + api_source = request.url_path + request_id = request.request_id + + def event_generator() -> List[LoggedEvent[Any]]: + timestamp = datetime.utcnow() + return [ + CustomEvent( + request_id=request_id, + api_source=api_source, + event_timestamp=timestamp, + event_data=event, + ) + for event in events + ] + + request_context.ensure_current_request().events.append(event_generator) diff --git a/wyvern/exceptions.py b/wyvern/exceptions.py new file mode 100644 index 0000000..6c35a13 --- /dev/null +++ b/wyvern/exceptions.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from typing import Optional + + +class WyvernError(Exception): + message = "Wyvern error" + + def __init__( + self, + message: Optional[str] = None, + error_code: int = 0, + **kwargs, + ) -> None: + self.error_code = error_code + if message: + self.message = message + try: + self._error_string = self.message.format(**kwargs) + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + + def __str__(self) -> str: + return f"{self.__class__.__name__}: {self._error_string}" + + +class WyvernEntityValidationError(WyvernError): + message = "{entity_key} is missing in entity data: {entity}" + + +class PaginationError(WyvernError): + pass + + +class WyvernRouteRegistrationError(WyvernError): + message = ( + "WyvernRouteRegistrationError: Invalid component: {component}. To register a route, " + "the component must be a subclass of APIComponentRoute" + ) + + +class WyvernFeatureStoreError(WyvernError): + message = "Received error from feature store: {error}" + + +class WyvernFeatureNameError(WyvernError): + message = ( + "Invalid online feature names: {invalid_feature_names}. " + "feature references must have format 'feature_view:feature', e.g. customer_fv:daily_transactions. " + "Are these realtime features? Make sure you define realtime feature component and register them." + ) + + +class WyvernModelInputError(WyvernError): + message = ( + "Invalid ModelInput: {model_input}" + "ModelInput.entities must contain at least one entity." + ) + + +class WyvernModelbitTokenMissingError(WyvernError): + message = "Modelbit authentication token is required." + + +class WyvernModelbitValidationError(WyvernError): + message = "Generated modelbit requests length does not match the number of target entities." + + +class WyvernAPIKeyMissingError(WyvernError): + message = ( + "Wyvern api key is missing. " + "Pass api_key to WyvernAPI or define WYVERN_API_KEY in your environment." + ) diff --git a/wyvern/feature_store/__init__.py b/wyvern/feature_store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/feature_store/constants.py b/wyvern/feature_store/constants.py new file mode 100644 index 0000000..09c78b3 --- /dev/null +++ b/wyvern/feature_store/constants.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +SQL_COLUMN_SEPARATOR = "__" +FULL_FEATURE_NAME_SEPARATOR = ":" diff --git a/wyvern/feature_store/feature_server.py b/wyvern/feature_store/feature_server.py new file mode 100644 index 0000000..4f29751 --- /dev/null +++ b/wyvern/feature_store/feature_server.py @@ -0,0 +1,424 @@ +# -*- coding: utf-8 -*- +import importlib +import logging +import time +import traceback +from collections import defaultdict +from typing import Any, Dict, List, Tuple + +import numpy as np +import pandas as pd +import uvicorn +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse +from feast import FeatureStore, proto_json +from feast.errors import EntityNotFoundException, FeatureViewNotFoundException +from feast.feature_store import _validate_entity_values, _validate_feature_refs +from feast.feature_view import ( + DUMMY_ENTITY, + DUMMY_ENTITY_ID, + DUMMY_ENTITY_NAME, + DUMMY_ENTITY_VAL, + FeatureView, +) +from feast.online_response import OnlineResponse +from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse +from feast.protos.feast.types.Value_pb2 import Value +from feast.type_map import python_values_to_proto_values +from feast.value_type import ValueType +from google.protobuf.json_format import MessageToDict + +from wyvern.components.features.realtime_features_component import ( + RealtimeFeatureComponent, +) +from wyvern.config import settings +from wyvern.feature_store.historical_feature_util import ( + build_historical_real_time_feature_requests, + build_historical_registry_feature_requests, + process_historical_real_time_features_requests, + process_historical_registry_features_requests, + separate_real_time_features, +) +from wyvern.feature_store.schemas import ( + GetHistoricalFeaturesRequest, + GetHistoricalFeaturesResponse, + GetOnlineFeaturesRequest, + MaterializeRequest, +) + +logger = logging.getLogger(__name__) +CRONJOB_INTERVAL_SECONDS = 60 * 5 # 5 minutes +CRONJOB_LOOKBACK_MINUTES = 12 # 12 mins +MAX_HISTORICAL_REQUEST_SIZE = 16000 + + +def _get_feature_views( + features: List[str], + all_feature_views: List[FeatureView], +) -> List[Tuple[FeatureView, List[str]]]: + # view name to view proto + view_index = {view.projection.name_to_use(): view for view in all_feature_views} + + # view name to feature names + views_features = defaultdict(set) + + for ref in features: + view_name, feat_name = ref.split(":") + if view_name in view_index: + view_index[view_name].projection.get_feature(feat_name) # For validation + views_features[view_name].add(feat_name) + else: + raise FeatureViewNotFoundException(view_name) + + fvs_result: List[Tuple[FeatureView, List[str]]] = [] + for view_name, feature_names in views_features.items(): + fvs_result.append((view_index[view_name], list(feature_names))) + return fvs_result + + +def generate_wyvern_store_app( + path: str, +) -> FastAPI: + proto_json.patch() + store = FeatureStore(repo_path=path) + app = FastAPI() + + provider = store._get_provider() + + importlib.import_module(".main", "pipelines") + + @app.exception_handler(Exception) + async def general_error_exception_handler(request: Request, exc: Exception): + return JSONResponse( + status_code=500, + content={"error": str(exc)}, + ) + + @app.middleware("http") + async def request_middleware(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + if request.url.path == "/healthcheck": + return response + process_time_ms = (time.time() - start_time) * 1000 + logger.info( + f"process_time={process_time_ms} ms, " + f"method={request.method}, url={request.url.path}, status_code={response.status_code}", + ) + return response + + @app.get("/healthcheck") + def healthcheck() -> Dict[str, str]: + return {"status": "ok"} + + @app.post(settings.WYVERN_ONLINE_FEATURES_PATH) + def get_online_features(data: GetOnlineFeaturesRequest) -> Dict[str, Any]: + try: + # Validate and parse the request data into GetOnlineFeaturesRequest Protobuf object + batch_sizes = [len(v) for v in data.entities.values()] + num_entities = batch_sizes[0] + if any(batch_size != num_entities for batch_size in batch_sizes): + raise HTTPException( + status_code=500, + detail="Uneven number of columns", + ) + _feature_refs = store._get_features(data.features, allow_cache=True) + + requested_feature_views = [ + *store._list_feature_views(True, False), + *store._registry.list_stream_feature_views( + project=store.project, + allow_cache=True, + ), + ] + + ( + entity_name_to_join_key_map, + entity_type_map, + join_keys_set, + ) = store._get_entity_maps(requested_feature_views) + # Convert values to Protobuf once. + entity_proto_values: Dict[str, List[Value]] = { + k: python_values_to_proto_values( + v, + entity_type_map.get(k, ValueType.UNKNOWN), + ) + for k, v in data.entities.items() + } + num_rows = _validate_entity_values(entity_proto_values) + + _validate_feature_refs(_feature_refs, data.full_feature_names) + grouped_refs = _get_feature_views( + _feature_refs, + requested_feature_views, + ) + # All requested features should be present in the result. + requested_result_row_names = { + feat_ref.replace(":", "__") for feat_ref in _feature_refs + } + if not data.full_feature_names: + requested_result_row_names = { + name.rpartition("__")[-1] for name in requested_result_row_names + } + + feature_views = list(view for view, _ in grouped_refs) + + join_key_values: Dict[str, List[Value]] = {} + for join_key_or_entity_name, values in entity_proto_values.items(): + if join_key_or_entity_name in join_keys_set: + join_key = join_key_or_entity_name + else: + try: + join_key = entity_name_to_join_key_map[join_key_or_entity_name] + except KeyError: + raise EntityNotFoundException( + join_key_or_entity_name, + store.project, + ) + else: + logger.warning( + "Using entity name is deprecated. Use join_key instead.", + ) + + # All join keys should be returned in the result. + requested_result_row_names.add(join_key) + join_key_values[join_key] = values + + # Populate online features response proto with join keys and request data features + online_features_response = GetOnlineFeaturesResponse(results=[]) + store._populate_result_rows_from_columnar( + online_features_response=online_features_response, + data=dict(**join_key_values), + ) + + # Add the Entityless case after populating result rows to avoid having to remove + # it later. + entityless_case = DUMMY_ENTITY_NAME in [ + entity_name + for feature_view in feature_views + for entity_name in feature_view.entities + ] + if entityless_case: + join_key_values[DUMMY_ENTITY_ID] = python_values_to_proto_values( + [DUMMY_ENTITY_VAL] * num_rows, + DUMMY_ENTITY.value_type, + ) + + for table, requested_features in grouped_refs: + # Get the correct set of entity values with the correct join keys. + table_entity_values, idxs = store._get_unique_entities( + table, + join_key_values, + entity_name_to_join_key_map, + ) + + # Fetch feature data for the minimum set of Entities. + feature_data = store._read_from_online_store( + table_entity_values, + provider, + requested_features, + table, + ) + + # Populate the result_rows with the Features from the OnlineStore inplace. + store._populate_response_from_feature_data( + feature_data, + idxs, + online_features_response, + data.full_feature_names, + requested_features, + table, + ) + store._drop_unneeded_columns( + online_features_response, + requested_result_row_names, + ) + response_proto = OnlineResponse(online_features_response).proto + + # Convert the Protobuf object to JSON and return it + result = MessageToDict( # type: ignore + response_proto, + preserving_proto_field_name=True, + float_precision=18, + ) + return result + except Exception as e: + # Print the original exception on the server side + logger.exception(traceback.format_exc()) + # Raise HTTPException to return the error message to the client + raise HTTPException(status_code=500, detail=str(e)) + + @app.post("/feature/materialize", status_code=201) + async def materialize(data: MaterializeRequest) -> None: + try: + logger.info(f"materialize called: {data}") + if data.start_date: + # materialize from start to end + store.materialize( + start_date=data.start_date, + end_date=data.end_date, + feature_views=data.feature_views, + ) + else: + store.materialize_incremental( + end_date=data.end_date, + feature_views=data.feature_views, + ) + store.refresh_registry() + logger.info("registry refreshed") + except Exception as e: + logger.exception(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) + + @app.post(settings.WYVERN_HISTORICAL_FEATURES_PATH) + async def get_historical_features( + data: GetHistoricalFeaturesRequest, + ) -> GetHistoricalFeaturesResponse: + """ + Wyvern feature store's historical features include realtime historical feature logged by wyvern pipeline and + offline historical features. + + Currently, we use the feast offline feature and only supports historical realtime features on snowflake. + """ + # validate the data input: lengths of requests, timestamps and all the entities should be the same + if "request" not in data.entities: + raise HTTPException( + status_code=400, + detail="request is required in entities", + ) + length_of_requests = len(data.entities["request"]) + length_of_timestamps = len(data.timestamps) + if length_of_requests != length_of_timestamps: + raise HTTPException( + status_code=400, + detail=( + f"Length of requests({length_of_requests}) and " + f"timestamps({length_of_timestamps}) should be the same" + ), + ) + if length_of_requests > MAX_HISTORICAL_REQUEST_SIZE: + raise HTTPException( + status_code=400, + detail=( + f"The max size of requests is {MAX_HISTORICAL_REQUEST_SIZE}. Got {length_of_requests} requests." + ), + ) + for key, value in data.entities.items(): + if len(value) != length_of_requests: + raise HTTPException( + status_code=400, + detail=f"Length of requests({length_of_requests}) and {key}({len(value)}) should be the same", + ) + + # convert the data input to pandas dataframe + data.entities["timestamp"] = data.timestamps + df = pd.DataFrame(data.entities) + realtime_features, feast_features = separate_real_time_features(data.features) + # TODO: analyze all the realtime features and generate all the composite feature columns in the dataframe + # the column name will be the composite feature name + valid_realtime_features: List[str] = [] + composite_entities: Dict[str, List[str]] = {} + for realtime_feature in realtime_features: + entity_type_column = RealtimeFeatureComponent.get_entity_type_column( + realtime_feature, + ) + entity_names = RealtimeFeatureComponent.get_entity_names(realtime_feature) + if not entity_type_column or not entity_names: + logger.warning(f"feature={realtime_feature} is not found") + continue + + if len(entity_names) == 2: + entity_name_1 = entity_names[0] + entity_name_2 = entity_names[1] + if entity_name_1 not in data.entities: + logger.warning( + f"Realtime feature {realtime_feature} depends on " + f"entity={entity_name_1}, which is not found in entities", + ) + continue + if entity_name_2 not in data.entities: + logger.warning( + f"Realtime feature {realtime_feature} depends on " + f"entity={entity_name_2}, which is not found in entities", + ) + continue + composite_entities[entity_type_column] = entity_names + valid_realtime_features.append(realtime_feature) + + # TODO: generate all the composite feature columns in the dataframe + for entity_type_column in composite_entities: + entity_name1, entity_name2 = composite_entities[entity_type_column] + df[entity_type_column] = df[entity_name1] + ":" + df[entity_name2] + + realtime_requests = build_historical_real_time_feature_requests( + full_feature_names=valid_realtime_features, + request_ids=data.entities["request"], + entities=data.entities, + ) + + real_time_responses = process_historical_real_time_features_requests( + requests=realtime_requests, + ) + for entity_identifier_type, features_df in real_time_responses.items(): + df = df.merge( + features_df, + left_on=["request", entity_identifier_type], + right_on=["REQUEST_ID", entity_identifier_type.upper()], + how="left", + ) + + feast_requests = build_historical_registry_feature_requests( + store=store, + feature_names=feast_features, + entity_values=data.entities, + timestamps=data.timestamps, + ) + feast_responses = process_historical_registry_features_requests( + store=store, + requests=feast_requests, + ) + + for feast_response in feast_responses: + if len(feast_response.IDENTIFIER) != len(df.REQUEST_ID): + raise HTTPException( + status_code=400, + detail=( + f"Length of feature store response({len(feast_response.IDENTIFIER)}) " + f"and request({len(df.REQUEST_ID)}) should be the same" + ), + ) + new_columns = [ + column + for column in feast_response.columns + if column not in ["IDENTIFIER", "event_timestamp"] + ] + df = df.join(feast_response[new_columns]) + + composite_keys = [key for key in composite_entities.keys() if key in df] + composite_keys_uppercase = [ + key.upper() for key in composite_entities.keys() if key.upper() in df + ] + drop_columns = composite_keys + composite_keys_uppercase + ["REQUEST_ID"] + df.drop(columns=drop_columns, inplace=True) + final_df = df.replace({np.nan: None}) + final_df["timestamp"] = final_df["timestamp"].astype(str) + + return GetHistoricalFeaturesResponse( + results=final_df.to_dict(orient="records"), + ) + + return app + + +def start_wyvern_store( + path: str, + host: str, + port: int, +): + app = generate_wyvern_store_app(path) + uvicorn.run( + app, + host=host, + port=port, + timeout_keep_alive=settings.FEATURE_STORE_TIMEOUT, + ) diff --git a/wyvern/feature_store/historical_feature_util.py b/wyvern/feature_store/historical_feature_util.py new file mode 100644 index 0000000..3529c2f --- /dev/null +++ b/wyvern/feature_store/historical_feature_util.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +import logging +from collections import defaultdict +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + +import more_itertools +import pandas as pd +from feast import FeatureStore +from snowflake.connector import SnowflakeConnection + +from wyvern.clients.snowflake import generate_snowflake_ctx +from wyvern.components.features.realtime_features_component import ( + RealtimeFeatureComponent, +) +from wyvern.feature_store.constants import ( + FULL_FEATURE_NAME_SEPARATOR, + SQL_COLUMN_SEPARATOR, +) +from wyvern.feature_store.schemas import ( + GetFeastHistoricalFeaturesRequest, + RequestEntityIdentifierObjects, +) + +logger = logging.getLogger(__name__) + + +def separate_real_time_features( + full_feature_names: Optional[List[str]], +) -> Tuple[List[str], List[str]]: + """ + Given a list of full feature names, separate real-time features and other features + """ + if full_feature_names is None: + return [], [] + + f_is_real_time_feature = ( + lambda feature: RealtimeFeatureComponent.get_entity_type_column(feature) + is not None + ) + other_feature_names, real_time_feature_names = more_itertools.partition( + f_is_real_time_feature, + full_feature_names, + ) + logger.debug(f"real_time_feature_names: {real_time_feature_names}") + logger.debug(f"other_feature_names: {other_feature_names}") + return list(real_time_feature_names), list(other_feature_names) + + +def build_historical_real_time_feature_requests( + full_feature_names: List[str], + request_ids: List[str], + entities: Dict[str, List[Any]], +) -> Dict[str, RequestEntityIdentifierObjects]: + features_grouped_by_entity = group_realtime_features_by_entity_type( + full_feature_names=full_feature_names, + ) + request_length = len(request_ids) + for feature_name, feature_identifiers in entities.items(): + if len(feature_identifiers) != request_length: + raise ValueError( + f"Number of {feature_name} features ({len(feature_identifiers)}) " + f"does not match number of requests ({request_length})", + ) + result_dict: Dict[str, RequestEntityIdentifierObjects] = {} + for ( + entity_identifier_type, + curr_feature_names, + ) in features_grouped_by_entity.items(): + entity_list = entity_identifier_type.split(SQL_COLUMN_SEPARATOR) + if len(entity_list) == 1: + # could just get all the entity_identifiers from the entities dict right away + result_dict[entity_identifier_type] = RequestEntityIdentifierObjects( + request_ids=request_ids, + entity_identifiers=entities[entity_identifier_type], + feature_names=curr_feature_names, + ) + elif len(entity_list) == 2: + # need to combine the entity_identifiers from the entities dict + list1 = entities[entity_list[0]] + list2 = entities[entity_list[1]] + result_dict[entity_identifier_type] = RequestEntityIdentifierObjects( + request_ids=request_ids, + entity_identifiers=[ + f"{list1[idx]}:{list2[idx]}" for idx in range(len(list1)) + ], + feature_names=curr_feature_names, + ) + return result_dict + + +def process_historical_real_time_features_requests( + requests: Dict[str, RequestEntityIdentifierObjects], +) -> Dict[str, pd.DataFrame]: + result: Dict[str, pd.DataFrame] = {} + with generate_snowflake_ctx() as context: + for entity_identifier_type, request in requests.items(): + result[ + entity_identifier_type + ] = process_historical_real_time_features_request( + entity_identifier_type=entity_identifier_type, + request=request, + context=context, + ) + return result + + +def process_historical_real_time_features_request( + entity_identifier_type: str, + request: RequestEntityIdentifierObjects, + context: SnowflakeConnection, +) -> pd.DataFrame: + """ + entity_identifier_type: "product__query" + """ + case_when_statements = [ + f"MAX(CASE WHEN FEATURE_NAME = '{feature_name}' THEN FEATURE_VALUE END) AS {feature_name.replace(':', '__')}" + for feature_name in request.feature_names + ] + # TODO (shu): the table name FEATURE_LOGS_PROD is hard-coded right now. Make this configurable or an env var. + query = f""" + SELECT + REQUEST_ID, + FEATURE_IDENTIFIER AS {entity_identifier_type}, + {",".join(case_when_statements)} + from FEATURE_LOGS_PROD + where + REQUEST_ID in ({','.join(['%s'] * len(request.request_ids))}) and + FEATURE_IDENTIFIER in ({','.join(['%s'] * len(request.entity_identifiers))}) + group by 1, 2 + """ + with context.cursor() as cursor: + # entity_identifiers is a list of strings as the parameters for the query + cursor.execute(query, request.request_ids + request.entity_identifiers) + return cursor.fetch_pandas_all() + + +def group_realtime_features_by_entity_type( + full_feature_names: List[str], +) -> Dict[str, List[str]]: + """ + Given a list of feature names, group them by their entity_identifier_type + """ + feature_entity_mapping: Dict[str, str] = {} + for full_feature_name in full_feature_names: + + # we want to use the column name which is using the separator __ + # because no ":" is allowed in the column name + entity_identifier_type = RealtimeFeatureComponent.get_entity_type_column( + full_feature_name, + ) + + if entity_identifier_type is None: + logger.warning(f"Could not find entity for feature: {full_feature_name}") + continue + feature_entity_mapping[full_feature_name] = entity_identifier_type + + logger.debug(f"feature_entity_mapping: {feature_entity_mapping}") + entity_feature_mapping: Dict[str, List[str]] = defaultdict(list) + for feature, entity_identifier_type in feature_entity_mapping.items(): + entity_feature_mapping[entity_identifier_type].append(feature) + logger.debug(f"entity_feature_mapping: {entity_feature_mapping}") + return entity_feature_mapping + + +def group_registry_features_by_entities( + full_feature_names: List[str], + store: FeatureStore, +) -> Dict[str, List[str]]: + entity_feature_mapping: Dict[str, List[str]] = defaultdict(list) + + # Precompute registry feature views and entity name mapping + fvs = store.registry.list_feature_views(project=store.project) + + for fv in fvs: + if len(fv.entities) > 1: + raise ValueError( + f"Feature view {fv.name} has more than one entity, which is not supported yet", + ) + entity_name = fv.entities[0].lower() + entity_feature_mapping[entity_name].extend( + feature_name + for feature_name in full_feature_names + if feature_name.startswith(f"{fv.name}:") + ) + + return entity_feature_mapping + + +def build_historical_registry_feature_requests( + store: FeatureStore, + feature_names: List[str], + entity_values: Dict[str, List[Any]], + timestamps: List[datetime], +) -> List[GetFeastHistoricalFeaturesRequest]: + features_grouped_by_entities = group_registry_features_by_entities( + feature_names, + store=store, + ) + requests: List[GetFeastHistoricalFeaturesRequest] = [] + for entity_name, feature_names in features_grouped_by_entities.items(): + if not feature_names: + continue + + if FULL_FEATURE_NAME_SEPARATOR in entity_name: + entities = entity_name.split(FULL_FEATURE_NAME_SEPARATOR) + else: + entities = [entity_name] + + if len(entities) > 2: + raise ValueError( + f"Entity name should be singular or composite: {entity_name}", + ) + for entity in entities: + if entity not in entity_values: + raise ValueError( + f"{feature_names} depends on {entity}. Could not find entity values: {entity}", + ) + request_entities: Dict[str, List[Any]] + if len(entities) == 1: + request_entities = { + "IDENTIFIER": entity_values[entities[0]], + } + else: + list1 = entity_values[entities[0]] + list2 = entity_values[entities[1]] + + request_entities = { + "IDENTIFIER": [ + f"{list1[idx]:{list2[idx]}}" for idx in range(len(list1)) + ], + } + request_entities["event_timestamp"] = timestamps + + requests.append( + GetFeastHistoricalFeaturesRequest( + features=feature_names, + entities=request_entities, + full_feature_names=True, + ), + ) + + return requests + + +def process_historical_registry_features_requests( + store: FeatureStore, + requests: List[GetFeastHistoricalFeaturesRequest], +) -> List[pd.DataFrame]: + """ + Given a list of historical feature requests, process them and return the results + """ + results = [] + for request in requests: + result = process_historical_registry_features_request(store, request) + results.append(result) + return results + + +def process_historical_registry_features_request( + store: FeatureStore, + request: GetFeastHistoricalFeaturesRequest, +) -> pd.DataFrame: + """ + Given a historical feature request, process it and return the results + """ + entity_df = pd.DataFrame(request.entities) + # no timezone is allowed in the timestamp + entity_df["event_timestamp"] = entity_df["event_timestamp"].dt.tz_localize(None) + result = store.get_historical_features( + entity_df=entity_df, + features=request.features or [], + full_feature_names=request.full_feature_names, + ) + result_df = result.to_df() + result_df.drop_duplicates(subset=["IDENTIFIER", "event_timestamp"], inplace=True) + return entity_df.merge( + result_df, + on=["IDENTIFIER", "event_timestamp"], + how="left", + ) diff --git a/wyvern/feature_store/schemas.py b/wyvern/feature_store/schemas.py new file mode 100644 index 0000000..0540305 --- /dev/null +++ b/wyvern/feature_store/schemas.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class GetOnlineFeaturesRequest(BaseModel): + entities: Dict[str, Any] = {} + features: List[str] = [] + full_feature_names: bool = False + + +class GetHistoricalFeaturesRequest(BaseModel): + entities: Dict[str, List[Any]] + timestamps: List[datetime] = [] + features: List[str] = [] + + +class GetFeastHistoricalFeaturesRequest(BaseModel): + full_feature_names: bool = False + entities: Dict[str, List[Any]] = {} + features: List[str] = [] + + +class GetHistoricalFeaturesResponse(BaseModel): + results: List[Dict[str, Any]] = [] + + +class MaterializeRequest(BaseModel): + end_date: datetime = Field(default_factory=datetime.utcnow) + feature_views: Optional[List[str]] = None + start_date: Optional[datetime] = None + + +class RequestEntityIdentifierObjects(BaseModel): + request_ids: List[str] = [] + entity_identifiers: List[str] = [] + feature_names: List[str] = [] diff --git a/wyvern/helper/__init__.py b/wyvern/helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/helper/sort.py b/wyvern/helper/sort.py new file mode 100644 index 0000000..4e31285 --- /dev/null +++ b/wyvern/helper/sort.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from enum import Enum + +from pydantic import BaseModel + + +class SortEnum(str, Enum): + asc = "asc" + desc = "desc" + + +class Sort(BaseModel): + sort_key: str + sort_field: str + sort_order: SortEnum = SortEnum.desc diff --git a/wyvern/index.py b/wyvern/index.py new file mode 100644 index 0000000..d2ad4ad --- /dev/null +++ b/wyvern/index.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict, List, Optional, Sequence + +from wyvern.redis import wyvern_redis + + +class WyvernIndex: + @classmethod + async def get(cls, entity_type: str, entity_id: str) -> Optional[Dict[str, Any]]: + return await wyvern_redis.get_entity( + entity_type=entity_type, + entity_id=entity_id, + ) + + @classmethod + async def bulk_get( + cls, + entity_type: str, + entity_ids: Sequence[str], + ) -> List[Optional[Dict[str, Any]]]: + if not entity_ids: + return [] + return await wyvern_redis.get_entities( + entity_type=entity_type, + entity_ids=entity_ids, + ) + + @classmethod + async def delete(cls, entity_type: str, entity_id: str) -> None: + await wyvern_redis.delete_entity( + entity_type=entity_type, + entity_id=entity_id, + ) + + @classmethod + async def bulk_delete( + cls, + entity_type: str, + entity_ids: Sequence[str], + ) -> None: + if not entity_ids: + return + await wyvern_redis.delete_entities( + entity_type=entity_type, + entity_ids=entity_ids, + ) + + +class WyvernEntityIndex: + @classmethod + async def get( + cls, + entity_type: str, + entity_id: str, + ) -> Optional[Dict[str, Any]]: + return await WyvernIndex.get( + entity_type=entity_type, + entity_id=entity_id, + ) + + @classmethod + async def bulk_get( + cls, + entity_type: str, + entity_ids: Sequence[str], + ) -> List[Optional[Dict[str, Any]]]: + return await WyvernIndex.bulk_get( + entity_type=entity_type, + entity_ids=entity_ids, + ) + + @classmethod + async def delete(cls, entity_type: str, entity_id: str) -> None: + await WyvernIndex.delete( + entity_type=entity_type, + entity_id=entity_id, + ) + + @classmethod + async def bulk_delete( + cls, + entity_type: str, + entity_ids: Sequence[str], + ) -> None: + await WyvernIndex.bulk_delete( + entity_type=entity_type, + entity_ids=entity_ids, + ) diff --git a/wyvern/redis.py b/wyvern/redis.py new file mode 100644 index 0000000..4a0ecec --- /dev/null +++ b/wyvern/redis.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +import logging +from typing import Any, Dict, List, Optional, Sequence + +from redis.asyncio import Redis + +from wyvern.config import settings +from wyvern.core.compression import wyvern_decode, wyvern_encode +from wyvern.utils import generate_index_key +from wyvern.wyvern_request import WyvernRequest + +logger = logging.getLogger(__name__) + +REDIS_BATCH_SIZE = settings.REDIS_BATCH_SIZE + + +class WyvernRedis: + """ + WyvernRedis is a wrapper for redis client to help index your entities in redis with Wyvern's convention + """ + + def __init__( + self, + scope: str = "", + redis_host: Optional[str] = None, + redis_port: Optional[int] = None, + ) -> None: + """ + scope is used to prefix the redis key. You can use the environment variable PROJECT_NAME to set the scope. + """ + host = redis_host or settings.REDIS_HOST + if not host: + raise ValueError("redis host is not set or found in environment variable") + port = redis_port or settings.REDIS_PORT + if not port: + raise ValueError("redis port is not set or found in environment variable") + self.redis_connection: Redis = Redis( + host=host, + port=port, + ) + self.key_prefix = scope or settings.PROJECT_NAME + + # TODO (shu): This entire file shouldn't be called redis.py -- this is specific to indexing + # We should actually have a redis.py file that does any of the required logic.. and mock that at most + async def bulk_index( + self, + entities: List[Dict[str, Any]], + entity_key: str, + entity_type: str, + ) -> List[str]: + if not entities: + return [] + mapping = { + generate_index_key( + self.key_prefix, + entity_type, + entity[entity_key], + ): wyvern_encode(entity) + for entity in entities + } + await self.redis_connection.mset(mapping=mapping) # type: ignore + return [entity[entity_key] for entity in entities] + + async def get(self, index_key: str) -> Optional[str]: + return await self.redis_connection.get(index_key) + + async def mget(self, index_keys: List[str]) -> List[Optional[str]]: + if not index_keys: + return [] + return await self.redis_connection.mget(index_keys) + + async def mget_json( + self, + index_keys: List[str], + ) -> List[Optional[Dict[str, Any]]]: + results = await self.mget(index_keys) + return [wyvern_decode(val) if val is not None else None for val in results] + + async def mget_update_in_place( + self, + index_keys: List[str], + wyvern_request: WyvernRequest, + ) -> None: + # single mget way + results = await self.mget(index_keys) + wyvern_request.entity_store = { + key: wyvern_decode(val) if val is not None else None + for key, val in zip(index_keys, results) + } + + async def get_entity( + self, + entity_type: str, + entity_id: str, + ) -> Optional[Dict[str, Any]]: + """ + get entity from redis + """ + index_key = generate_index_key( + self.key_prefix, + entity_type, + entity_id, + ) + + encoded_entity = await self.get(index_key) + if not encoded_entity: + return None + return wyvern_decode(encoded_entity) + + async def get_entities( + self, + entity_type: str, + entity_ids: Sequence[str], + ) -> List[Optional[Dict[str, Any]]]: + """ + get entity from redis + """ + index_keys = [ + generate_index_key(self.key_prefix, entity_type, entity_id) + for entity_id in entity_ids + ] + if not index_keys: + return [] + return await self.mget_json(index_keys) + + async def delete_entity( + self, + entity_type: str, + entity_id: str, + ) -> None: + """ + delete entity from redis + """ + index_key = generate_index_key(self.key_prefix, entity_type, entity_id) + await self.redis_connection.delete(index_key) + + async def delete_entities( + self, + entity_type: str, + entity_ids: Sequence[str], + ) -> None: + """ + delete entities from redis + """ + index_keys = [ + generate_index_key(self.key_prefix, entity_type, entity_id) + for entity_id in entity_ids + ] + await self.redis_connection.delete(*index_keys) + + +wyvern_redis = WyvernRedis() diff --git a/wyvern/request_context.py b/wyvern/request_context.py new file mode 100644 index 0000000..d3e1d6e --- /dev/null +++ b/wyvern/request_context.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from contextvars import ContextVar +from typing import Optional + +from wyvern.wyvern_request import WyvernRequest + +_request_context: ContextVar[Optional[WyvernRequest]] = ContextVar( + "Global request context", + default=None, +) + + +def current() -> Optional[WyvernRequest]: + return _request_context.get() + + +def ensure_current_request() -> WyvernRequest: + request = current() + if request is None: + raise RuntimeError("No wyvern request context") + return request + + +def set(request: WyvernRequest) -> None: + _request_context.set(request) + + +def reset() -> None: + _request_context.set(None) diff --git a/wyvern/service.py b/wyvern/service.py new file mode 100644 index 0000000..5dced14 --- /dev/null +++ b/wyvern/service.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import asyncio +from typing import List, Optional, Type + +from dotenv import load_dotenv +from fastapi import FastAPI + +from wyvern.components.api_route_component import APIRouteComponent +from wyvern.components.features.realtime_features_component import ( + RealtimeFeatureComponent, +) +from wyvern.components.index import ( + IndexDeleteComponent, + IndexGetComponent, + IndexUploadComponent, +) +from wyvern.web_frameworks.fastapi import WyvernFastapi + + +class WyvernService: + def __init__( + self, + *, + host: str = "127.0.0.1", + port: int = 5000, + ) -> None: + self.host = host + self.port = port + self.service = WyvernFastapi(host=self.host, port=self.port) + + async def register_routes( + self, + route_components: List[Type[APIRouteComponent]], + ) -> None: + for route_component in route_components: + await self.service.register_route(route_component=route_component) + + def _run( + self, + ) -> None: + load_dotenv() + self.service.run() + + @staticmethod + def generate( + *, + route_components: Optional[List[Type[APIRouteComponent]]] = None, + realtime_feature_components: Optional[ + List[Type[RealtimeFeatureComponent]] + ] = None, + host: str = "127.0.0.1", + port: int = 5000, + ) -> WyvernService: + route_components = route_components or [] + service = WyvernService(host=host, port=port) + asyncio.run( + service.register_routes( + [ + IndexDeleteComponent, + IndexGetComponent, + IndexUploadComponent, + *route_components, + ], + ), + ) + return service + + @staticmethod + def run( + *, + route_components: List[Type[APIRouteComponent]], + realtime_feature_components: Optional[ + List[Type[RealtimeFeatureComponent]] + ] = None, + host: str = "127.0.0.1", + port: int = 5000, + ): + service = WyvernService.generate( + route_components=route_components, + realtime_feature_components=realtime_feature_components, + host=host, + port=port, + ) + service._run() + + @staticmethod + def generate_app( + *, + route_components: Optional[List[Type[APIRouteComponent]]] = None, + realtime_feature_components: Optional[ + List[Type[RealtimeFeatureComponent]] + ] = None, + host: str = "127.0.0.1", + port: int = 5000, + ) -> FastAPI: + service = WyvernService.generate( + route_components=route_components, + realtime_feature_components=realtime_feature_components, + host=host, + port=port, + ) + return service.service.app diff --git a/wyvern/utils.py b/wyvern/utils.py new file mode 100644 index 0000000..c0dd72b --- /dev/null +++ b/wyvern/utils.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from wyvern.config import settings + + +def generate_index_key( + scope: str, + entity_type: str, + entity_id: str, +) -> str: + return f"{scope}:{settings.WYVERN_INDEX_VERSION}:{entity_type}:{entity_id}" diff --git a/wyvern/web_frameworks/__init__.py b/wyvern/web_frameworks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wyvern/web_frameworks/fastapi.py b/wyvern/web_frameworks/fastapi.py new file mode 100644 index 0000000..4f1e793 --- /dev/null +++ b/wyvern/web_frameworks/fastapi.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +import logging +import time +from contextlib import asynccontextmanager +from typing import Dict, Type + +import uvicorn +from ddtrace import tracer +from fastapi import BackgroundTasks, FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse +from pydantic import ValidationError + +from wyvern import request_context +from wyvern.aws.kinesis import KinesisFirehoseStream, wyvern_kinesis_firehose +from wyvern.components.api_route_component import APIRouteComponent +from wyvern.config import settings +from wyvern.core.httpx import httpx_client +from wyvern.entities.request import BaseWyvernRequest +from wyvern.event_logging import event_logger +from wyvern.exceptions import WyvernError, WyvernRouteRegistrationError +from wyvern.wyvern_request import WyvernRequest + +logger = logging.getLogger(__name__) + + +def _dedupe_slash(path: str) -> str: + return path.replace("//", "/") + + +def _massage_path(path: str) -> str: + massaged_path = _dedupe_slash(path) + if massaged_path and massaged_path[0] != "/": + massaged_path = "/" + massaged_path + if massaged_path and massaged_path[-1] == "/": + massaged_path = massaged_path[:-1] + return massaged_path + + +@asynccontextmanager +async def lifespan(app: FastAPI): + try: + httpx_client.start() + yield + finally: + await httpx_client.stop() + + +class WyvernFastapi: + """ + endpoint input: + the built WyvernPipeline + the request input schema + the request output schema + """ + + def __init__(self, host: str = "127.0.0.1", port: int = 8000) -> None: + self.app = FastAPI(lifespan=lifespan) + self.host = host + self.port = port + + @self.app.exception_handler(WyvernError) + async def wyvern_exception_handler(request: Request, exc: WyvernError): + return JSONResponse( + status_code=400, + content={ + "detail": str(exc), + "error_code": exc.error_code, + }, + ) + + @self.app.get("/healthcheck") + async def healthcheck() -> Dict[str, str]: + return {"status": "OK"} + + @self.app.middleware("http") + async def request_middleware(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + if request.url.path == "/healthcheck": + return response + process_time_ms = (time.time() - start_time) * 1000 + logger.info( + f"process_time={process_time_ms} ms, " + f"method={request.method}, url={request.url.path}, status_code={response.status_code}", + ) + return response + + async def register_route( + self, + route_component: Type[APIRouteComponent], + ) -> None: + """ + Support post request + """ + if not issubclass(route_component, APIRouteComponent): + raise WyvernRouteRegistrationError(component=route_component) + + root_component = route_component() + await root_component.initialize_wrapper() + path = _massage_path(f"/api/{root_component.API_VERSION}/{root_component.PATH}") + + @self.app.post( + path, + response_model=root_component.RESPONSE_SCHEMA_CLASS, + response_model_exclude_none=True, + ) + async def post( + fastapi_request: Request, + background_tasks: BackgroundTasks, + ) -> root_component.RESPONSE_SCHEMA_CLASS: # type: ignore + with tracer.trace("wyvern.get_json"): + json = await fastapi_request.json() + + try: + with tracer.trace("wyvern.parse_json"): + data = root_component.REQUEST_SCHEMA_CLASS(**json) + # from pyinstrument import Profiler + # profiler = Profiler(async_mode="enabled") + # profiler.start() + request_id = None + if isinstance(data, BaseWyvernRequest): + request_id = data.request_id + wyvern_req = WyvernRequest.parse_fastapi_request( + json=data, + req=fastapi_request, + request_id=request_id, + ) + request_context.set(wyvern_req) + + await root_component.warm_up(data) + output = await root_component.execute(data) + + background_tasks.add_task( + wyvern_kinesis_firehose.put_record_batch_callable, + KinesisFirehoseStream.EVENT_STREAM, + # TODO (suchintan): "invariant" list error + event_logger.get_logged_events_generator(), # type: ignore + ) + + # profiler.stop() + # profiler.print(show_all=True) + except ValidationError as e: + logger.exception(f"Unexpected error error={e} request_payload={json}") + raise HTTPException(status_code=422, detail=e.errors()) + except Exception as e: + logger.exception(f"Unexpected error error={e} request_payload={json}") + raise HTTPException(status_code=500, detail=str(e)) + finally: + request_context.reset() + if not output: + raise HTTPException(status_code=500, detail="something is wrong") + logger.info( + f"path={path}, request_payload={json}, response_payload={output}", + ) + return output + + def run(self) -> None: + config = uvicorn.Config( + self.app, + host=self.host, + port=self.port, + timeout_keep_alive=settings.SERVER_TIMEOUT, + ) + uvicorn_server = uvicorn.Server(config=config) + uvicorn_server.run() diff --git a/wyvern/wyvern_logging.py b/wyvern/wyvern_logging.py new file mode 100644 index 0000000..e044aec --- /dev/null +++ b/wyvern/wyvern_logging.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import logging +import logging.config +import os + +import yaml + +logger = logging.getLogger(__name__) + + +def setup_logging(): + # this log_config.yml file path is changed compared to the original library code + path = os.path.abspath("log_config.yml") + + if os.path.exists(path): + with open(path, "rt") as f: + try: + config = yaml.safe_load(f.read()) + + # logfile_path = config["handlers"]["file"]["filename"] + # os.makedirs(logfile_path, exist_ok=True) + logging.config.dictConfig(config) + except Exception as e: + logger.error("Error in Logging Configuration. Using default configs") + raise e + # logging.basicConfig(level=logging.INFO) + else: + logging.basicConfig(level=logging.INFO) + logger.warning("Failed to load configuration file. Using default configs") diff --git a/wyvern/wyvern_request.py b/wyvern/wyvern_request.py new file mode 100644 index 0000000..248b75b --- /dev/null +++ b/wyvern/wyvern_request.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional +from urllib.parse import urlparse + +import fastapi +from pydantic import BaseModel + +from wyvern.components.events.events import LoggedEvent +from wyvern.entities.feature_entities import FeatureMap + + +@dataclass +class WyvernRequest: + method: str + url: str + url_path: str + json: BaseModel + headers: Dict[Any, Any] + + entity_store: Dict[str, Optional[Dict[str, Any]]] + # TODO (suchintan): Validate that there is no thread leakage here + # The list of list here is a minor performance optimization to prevent copying of lists for events + events: List[Callable[[], List[LoggedEvent[Any]]]] + + feature_map: FeatureMap + + request_id: Optional[str] = None + + # TODO: params + + @classmethod + def parse_fastapi_request( + cls, + json: BaseModel, + req: fastapi.Request, + request_id: Optional[str] = None, + ) -> WyvernRequest: + return cls( + method=req.method, + url=str(req.url), + url_path=urlparse(str(req.url)).path, + json=json, + headers=dict(req.headers), + entity_store={}, + events=[], + feature_map=FeatureMap(feature_map={}), + request_id=request_id, + ) diff --git a/wyvern/wyvern_tracing.py b/wyvern/wyvern_tracing.py new file mode 100644 index 0000000..272622c --- /dev/null +++ b/wyvern/wyvern_tracing.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from ddtrace import tracer +from ddtrace.filters import FilterRequestsOnUrl + + +def setup_tracing(): + tracer.configure( + settings={ + "FILTERS": [ + FilterRequestsOnUrl(r"http://.*/healthcheck$"), + ], + }, + ) diff --git a/wyvern/wyvern_typing.py b/wyvern/wyvern_typing.py new file mode 100644 index 0000000..f98f734 --- /dev/null +++ b/wyvern/wyvern_typing.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from typing import List, TypeVar, Union + +from pydantic import BaseModel + +from wyvern.entities.identifier_entities import WyvernEntity +from wyvern.entities.request import BaseWyvernRequest + +T = TypeVar("T") +REQUEST_ENTITY = TypeVar("REQUEST_ENTITY", bound=BaseWyvernRequest) +WYVERN_ENTITY = TypeVar("WYVERN_ENTITY", bound=WyvernEntity) +GENERALIZED_WYVERN_ENTITY = TypeVar( + "GENERALIZED_WYVERN_ENTITY", + bound=Union[WyvernEntity, BaseWyvernRequest], +) +INPUT_TYPE = TypeVar("INPUT_TYPE") +OUTPUT_TYPE = TypeVar("OUTPUT_TYPE") +UPSTREAM_INPUT_TYPE = TypeVar("UPSTREAM_INPUT_TYPE") +UPSTREAM_OUTPUT_TYPE = TypeVar("UPSTREAM_OUTPUT_TYPE") +REQUEST_SCHEMA = TypeVar("REQUEST_SCHEMA", bound=BaseModel) +RESPONSE_SCHEMA = TypeVar("RESPONSE_SCHEMA", bound=BaseModel) + +WyvernFeature = Union[float, str, List[float], None]