From 79ba18e15fc501cffbf7d273bc1576bd92f38587 Mon Sep 17 00:00:00 2001 From: Declan Date: Tue, 3 Oct 2023 22:13:42 +0100 Subject: [PATCH] initial commit --- .gitattributes | 1 + .gitignore | 162 ++++++++ LICENSE | 21 + README.md | 33 ++ docs/img-1.png | Bin 0 -> 40800 bytes poetry.lock | 644 +++++++++++++++++++++++++++++ pyproject.toml | 26 ++ request_coalescing_py/__init__.py | 0 request_coalescing_py/coalescer.py | 33 ++ request_coalescing_py/database.py | 37 ++ request_coalescing_py/main.py | 33 ++ request_coalescing_py/models.py | 6 + request_coalescing_py/routes.py | 40 ++ tests/__init__.py | 0 tests/conftest.py | 24 ++ tests/test_coalesced.py | 25 ++ tests/test_standard.py | 25 ++ 17 files changed, 1110 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/img-1.png create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 request_coalescing_py/__init__.py create mode 100644 request_coalescing_py/coalescer.py create mode 100644 request_coalescing_py/database.py create mode 100644 request_coalescing_py/main.py create mode 100644 request_coalescing_py/models.py create mode 100644 request_coalescing_py/routes.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_coalesced.py create mode 100644 tests/test_standard.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a272b3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# 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/ +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/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .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 + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +test.db \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c3dc55c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Declan Teevan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8c66e2 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Request Coalescing in Async Python + +## About + +This repository is a simple experiment revolved around implementing request coalescing in Python using Asyncio and FastAPI, inspired by the Discord Engineering team's blog post on [How Discord Stores Trillions of Messages.](https://discord.com/blog/how-discord-stores-trillions-of-messages) + +## How does it work? + +When a client makes a request for an item, as they're name in this demo, of a specific id, it adds a task to the coalescer queue and waits for the result of that task. Any subsequent requests for items of the same ID, in the meantime, will subscribe to the future result of that first pending task instead of performing their own read query. + +![Coalescing Diagram](/docs/img-1.png) + +## Testing + +```bash +> poetry run pytest + +tests\test_standard.py Making 5x100 concurrent requests (500 total)... +Standard Requests: Took 457.228ms +Standard Metrics: {'requests': 500, 'db_calls': 500} + +tests\test_coalesced.py Making 5x100 concurrent requests (500 total)... +Coalesced Requests: Took 49.328ms +Coalesced Metrics: {'requests': 500, 'db_calls': 100} +``` + +The number of database queries has fallen from 1:1 with requests to 1 database call per 5 requests (lining up with there being 5 requests being made concurrently in the tests). + +## License and Contributing + +Please feel free to make pull requests containing any suggestions for improvements. + +This repository is open sourced under the [MIT License.](/LICENSE) \ No newline at end of file diff --git a/docs/img-1.png b/docs/img-1.png new file mode 100644 index 0000000000000000000000000000000000000000..404a96f5b754ae55bc02e3d2e75227f9cd0abd7f GIT binary patch literal 40800 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 A^8f$< literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..4064a78 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,644 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "aiosqlite" +version = "0.19.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, + {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, +] + +[package.extras] +dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] +docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] + +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.7" +files = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[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 = "asgi-lifespan" +version = "2.1.0" +description = "Programmatic startup/shutdown of ASGI apps." +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308"}, + {file = "asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f"}, +] + +[package.dependencies] +sniffio = "*" + +[[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 = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[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 = "databases" +version = "0.8.0" +description = "Async database support for Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "databases-0.8.0-py3-none-any.whl", hash = "sha256:0ceb7fd5c740d846e1f4f58c0256d780a6786841ec8e624a21f1eb1b51a9093d"}, + {file = "databases-0.8.0.tar.gz", hash = "sha256:6544d82e9926f233d694ec29cd018403444c7fb6e863af881a8304d1ff5cfb90"}, +] + +[package.dependencies] +aiosqlite = {version = "*", optional = true, markers = "extra == \"sqlite\""} +sqlalchemy = ">=1.4.42,<1.5" + +[package.extras] +aiomysql = ["aiomysql"] +aiopg = ["aiopg"] +aiosqlite = ["aiosqlite"] +asyncmy = ["asyncmy"] +asyncpg = ["asyncpg"] +mysql = ["aiomysql"] +postgresql = ["asyncpg"] +sqlite = ["aiosqlite"] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.103.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.103.2-py3-none-any.whl", hash = "sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e"}, + {file = "fastapi-0.103.2.tar.gz", hash = "sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653"}, +] + +[package.dependencies] +anyio = ">=3.7.1,<4.0.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.27.0,<0.28.0" +typing-extensions = ">=4.5.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "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)"] + +[[package]] +name = "greenlet" +version = "3.0.0" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e09dea87cc91aea5500262993cbd484b41edf8af74f976719dd83fe724644cd6"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdfaeecf8cc705d35d8e6de324bf58427d7eafb55f67050d8f28053a3d57118c"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a68d670c8f89ff65c82b936275369e532772eebc027c3be68c6b87ad05ca695"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ad562a104cd41e9d4644f46ea37167b93190c6d5e4048fcc4b80d34ecb278f"}, + {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a"}, + {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b1660a15a446206c8545edc292ab5c48b91ff732f91b3d3b30d9a915d5ec4779"}, + {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:813720bd57e193391dfe26f4871186cf460848b83df7e23e6bef698a7624b4c9"}, + {file = "greenlet-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa15a2ec737cb609ed48902b45c5e4ff6044feb5dcdfcf6fa8482379190330d7"}, + {file = "greenlet-3.0.0-cp310-universal2-macosx_11_0_x86_64.whl", hash = "sha256:7709fd7bb02b31908dc8fd35bfd0a29fc24681d5cc9ac1d64ad07f8d2b7db62f"}, + {file = "greenlet-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14"}, + {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b"}, + {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"}, + {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"}, + {file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"}, + {file = "greenlet-3.0.0-cp311-universal2-macosx_10_9_universal2.whl", hash = "sha256:c3692ecf3fe754c8c0f2c95ff19626584459eab110eaab66413b1e7425cd84e9"}, + {file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d363666acc21d2c204dd8705c0e0457d7b2ee7a76cb16ffc099d6799744ac99"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:334ef6ed8337bd0b58bb0ae4f7f2dcc84c9f116e474bb4ec250a8bb9bd797a66"}, + {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6672fdde0fd1a60b44fb1751a7779c6db487e42b0cc65e7caa6aa686874e79fb"}, + {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"}, + {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"}, + {file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"}, + {file = "greenlet-3.0.0-cp312-universal2-macosx_10_9_universal2.whl", hash = "sha256:553d6fb2324e7f4f0899e5ad2c427a4579ed4873f42124beba763f16032959af"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52a712c38e5fb4fd68e00dc3caf00b60cb65634d50e32281a9d6431b33b4af1"}, + {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5539f6da3418c3dc002739cb2bb8d169056aa66e0c83f6bacae0cd3ac26b423"}, + {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:343675e0da2f3c69d3fb1e894ba0a1acf58f481f3b9372ce1eb465ef93cf6fed"}, + {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:abe1ef3d780de56defd0c77c5ba95e152f4e4c4e12d7e11dd8447d338b85a625"}, + {file = "greenlet-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:e693e759e172fa1c2c90d35dea4acbdd1d609b6936115d3739148d5e4cd11947"}, + {file = "greenlet-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bdd696947cd695924aecb3870660b7545a19851f93b9d327ef8236bfc49be705"}, + {file = "greenlet-3.0.0-cp37-universal2-macosx_11_0_x86_64.whl", hash = "sha256:cc3e2679ea13b4de79bdc44b25a0c4fcd5e94e21b8f290791744ac42d34a0353"}, + {file = "greenlet-3.0.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:63acdc34c9cde42a6534518e32ce55c30f932b473c62c235a466469a710bfbf9"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a1a6244ff96343e9994e37e5b4839f09a0207d35ef6134dce5c20d260d0302c"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b822fab253ac0f330ee807e7485769e3ac85d5eef827ca224feaaefa462dc0d0"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8060b32d8586e912a7b7dac2d15b28dbbd63a174ab32f5bc6d107a1c4143f40b"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:621fcb346141ae08cb95424ebfc5b014361621b8132c48e538e34c3c93ac7365"}, + {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb36985f606a7c49916eff74ab99399cdfd09241c375d5a820bb855dfb4af9f"}, + {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10b5582744abd9858947d163843d323d0b67be9432db50f8bf83031032bc218d"}, + {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f"}, + {file = "greenlet-3.0.0-cp38-cp38-win32.whl", hash = "sha256:9de687479faec7db5b198cc365bc34addd256b0028956501f4d4d5e9ca2e240a"}, + {file = "greenlet-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:3fd2b18432e7298fcbec3d39e1a0aa91ae9ea1c93356ec089421fabc3651572b"}, + {file = "greenlet-3.0.0-cp38-universal2-macosx_11_0_x86_64.whl", hash = "sha256:3c0d36f5adc6e6100aedbc976d7428a9f7194ea79911aa4bf471f44ee13a9464"}, + {file = "greenlet-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4cd83fb8d8e17633ad534d9ac93719ef8937568d730ef07ac3a98cb520fd93e4"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5b2d4cdaf1c71057ff823a19d850ed5c6c2d3686cb71f73ae4d6382aaa7a06"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e7dcdfad252f2ca83c685b0fa9fba00e4d8f243b73839229d56ee3d9d219314"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94e4e924d09b5a3e37b853fe5924a95eac058cb6f6fb437ebb588b7eda79870"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6fb737e46b8bd63156b8f59ba6cdef46fe2b7db0c5804388a2d0519b8ddb99"}, + {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d55db1db455c59b46f794346efce896e754b8942817f46a1bada2d29446e305a"}, + {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56867a3b3cf26dc8a0beecdb4459c59f4c47cdd5424618c08515f682e1d46692"}, + {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a812224a5fb17a538207e8cf8e86f517df2080c8ee0f8c1ed2bdaccd18f38f4"}, + {file = "greenlet-3.0.0-cp39-cp39-win32.whl", hash = "sha256:0d3f83ffb18dc57243e0151331e3c383b05e5b6c5029ac29f754745c800f8ed9"}, + {file = "greenlet-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:831d6f35037cf18ca5e80a737a27d822d87cd922521d18ed3dbc8a6967be50ce"}, + {file = "greenlet-3.0.0-cp39-universal2-macosx_11_0_x86_64.whl", hash = "sha256:a048293392d4e058298710a54dfaefcefdf49d287cd33fb1f7d63d55426e4355"}, + {file = "greenlet-3.0.0.tar.gz", hash = "sha256:19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b"}, +] + +[package.extras] +docs = ["Sphinx"] +test = ["objgraph", "psutil"] + +[[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 = "httpcore" +version = "0.18.0" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"}, + {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"}, +] + +[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 = "httpx" +version = "0.25.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, + {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, +] + +[package.dependencies] +certifi = "*" +httpcore = ">=0.18.0,<0.19.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 = "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 = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.4.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.10.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.10.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, + {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, + {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[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 = "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 = "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 == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} + +[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 = "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 = "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 = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "uvicorn" +version = "0.23.2" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, + {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[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)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "4768622928d63eff0a159f569efcfe9b65ae154f8f2c60cbe6853b9f873e2f65" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..86d20ca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "request-coalescing-py" +version = "0.1.0" +description = "A simple demonstration of request coalescing in asynchronous Python." +authors = ["Declan "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.8" +fastapi = "^0.103.2" +pydantic = "^2.4.2" +databases = {extras = ["sqlite"], version = "^0.8.0"} +uvicorn = "^0.23.2" + +[tool.poetry.group.dev.dependencies] +httpx = "^0.25.0" +pytest = "^7.4.2" +asgi-lifespan = "^2.1.0" + +[tool.pytest.ini_options] +minversion = "7.0" +addopts = "--capture=tee-sys -p no:cacheprovider" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/request_coalescing_py/__init__.py b/request_coalescing_py/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/request_coalescing_py/coalescer.py b/request_coalescing_py/coalescer.py new file mode 100644 index 0000000..16e7ecf --- /dev/null +++ b/request_coalescing_py/coalescer.py @@ -0,0 +1,33 @@ +import asyncio +from typing import Optional + +from request_coalescing_py.models import Item +from request_coalescing_py.database import DatabaseRepo + + +class CoalescingRepo: + def __init__(self, repo: DatabaseRepo): + self._repo = repo + + self._queue = asyncio.Queue() + self._queued = {} # map of item_id: future + + async def get_by_id(self, item_id: int) -> "asyncio.Future[Optional[Item]]": + # Check if there is an already pending request for that item. + fut = self._queued.get(item_id) + if fut: + return fut + + # There is not a pending request. + fut = asyncio.get_event_loop().create_future() + self._queued[item_id] = fut + await self._queue.put(item_id) + return fut + + async def process_queue(self) -> None: + while True: + item_id = await self._queue.get() + item = await self._repo.get_by_id(item_id) + self._queued[item_id].set_result(item) + del self._queued[item_id] + self._queue.task_done() diff --git a/request_coalescing_py/database.py b/request_coalescing_py/database.py new file mode 100644 index 0000000..97fc28c --- /dev/null +++ b/request_coalescing_py/database.py @@ -0,0 +1,37 @@ +import asyncio +from typing import Optional + +from fastapi import FastAPI +from databases import Database + +from request_coalescing_py.models import Item + + +class DatabaseRepo: + def __init__(self, app: FastAPI) -> None: + self.app = app + + async def start_db(self) -> None: + self._db = Database("sqlite://./test.db") + await self._db.connect() + + try: + await self._db.execute(query="CREATE TABLE IF NOT EXISTS 'items' (id INTEGER PRIMARY KEY, name TEXT)") + await self._db.execute(query="INSERT INTO 'items' (id, name) VALUES (1, 'Test Item')") + except Exception: + pass + + async def stop_db(self) -> None: + await self._db.disconnect() + + async def get_by_id(self, item_id: int) -> Optional[Item]: + self.app.state.metrics["db_calls"] += 1 + + # Simulate expensive read (50ms) + await asyncio.sleep(.05) + + row = await self._db.fetch_one(query="SELECT * FROM 'items' WHERE id = :id", values={"id": item_id}) + if row: + return Item(**row._mapping) + + return None diff --git a/request_coalescing_py/main.py b/request_coalescing_py/main.py new file mode 100644 index 0000000..f0d9e23 --- /dev/null +++ b/request_coalescing_py/main.py @@ -0,0 +1,33 @@ +import asyncio + +from fastapi import FastAPI + +from request_coalescing_py.database import DatabaseRepo +from request_coalescing_py.coalescer import CoalescingRepo +from request_coalescing_py.routes import router + +app = FastAPI() + + +@app.on_event("startup") +async def startup_event(): + # initialise metrics + app.state.DEFAULT_METRICS = {"requests": 0, "db_calls": 0} + app.state.metrics = app.state.DEFAULT_METRICS.copy() + + # initilise DB repository + app.state.repo = DatabaseRepo(app=app) + await app.state.repo.start_db() + + # initialise worker and coalescing repo + app.state.coalescer = CoalescingRepo(repo=app.state.repo) + asyncio.create_task(app.state.coalescer.process_queue()) + + +@app.on_event("shutdown") +async def shutdown_event(): + # close DB connection + await app.state.repo.stop_db() + + +app.include_router(router) \ No newline at end of file diff --git a/request_coalescing_py/models.py b/request_coalescing_py/models.py new file mode 100644 index 0000000..a5230e8 --- /dev/null +++ b/request_coalescing_py/models.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class Item(BaseModel): + id: int + name: str \ No newline at end of file diff --git a/request_coalescing_py/routes.py b/request_coalescing_py/routes.py new file mode 100644 index 0000000..945cb65 --- /dev/null +++ b/request_coalescing_py/routes.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Request, HTTPException + +from request_coalescing_py.models import Item + +router = APIRouter() + + +@router.get("/metrics") +def view_metrics(request: Request) -> dict: + return request.app.state.metrics + + +@router.post("/metrics") +def view_and_reset_metrics(request: Request) -> dict: + metrics = request.app.state.metrics + request.app.state.metrics = request.app.state.DEFAULT_METRICS.copy() + return metrics + + +@router.get("/standard/{item_id}") +async def get_standard_route(request: Request, item_id: int) -> Item: + request.app.state.metrics["requests"] += 1 + + item = await request.app.state.repo.get_by_id(item_id) + if item is None: + raise HTTPException(status_code=404, detail="Item Not Found") + + return item + + +@router.get("/coalesced/{item_id}") +async def get_coalesced_route(request: Request, item_id: int) -> Item: + request.app.state.metrics["requests"] += 1 + + item_future = await request.app.state.coalescer.get_by_id(item_id) + item = await item_future + if item is None: + raise HTTPException(status_code=404, detail="Item Not Found") + + return item \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8a9479c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,24 @@ +import pytest +from fastapi import FastAPI +from httpx import AsyncClient +from asgi_lifespan import LifespanManager + +from request_coalescing_py.main import app +from request_coalescing_py.models import Item + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +@pytest.fixture(scope="function") +async def test_app() -> FastAPI: + async with LifespanManager(app) as manager: + yield manager.app + + +@pytest.fixture(scope="function") +async def client(test_app) -> AsyncClient: + async with AsyncClient(app=test_app, base_url="http://test") as client: + yield client \ No newline at end of file diff --git a/tests/test_coalesced.py b/tests/test_coalesced.py new file mode 100644 index 0000000..8f2ae07 --- /dev/null +++ b/tests/test_coalesced.py @@ -0,0 +1,25 @@ +import asyncio +from datetime import datetime + +import pytest + + +@pytest.mark.anyio +async def test_coalesced_requests(client) -> None: + print("Making 5x100 concurrent requests (500 total)...") + + async def make_requests(): + for _ in range(100): + response = await client.get("/coalesced/1") + assert response.status_code == 200 + + # Make the requests concurrently and wait until they complete + start_time = datetime.now() + tasks = [asyncio.create_task(make_requests()) for _ in range(5)] + for task in tasks: + await task + print("Coalesced Requests: Took {delta}ms".format(delta=(datetime.now() - start_time).microseconds / 1000)) + + # View the metrics + metrics = await client.get("/metrics") + print("Coalesced Metrics:", metrics.json()) diff --git a/tests/test_standard.py b/tests/test_standard.py new file mode 100644 index 0000000..13c3072 --- /dev/null +++ b/tests/test_standard.py @@ -0,0 +1,25 @@ +import asyncio +from datetime import datetime + +import pytest + + +@pytest.mark.anyio +async def test_standard_requests(client) -> None: + print("Making 5x100 concurrent requests (500 total)...") + + async def make_requests(): + for _ in range(100): + response = await client.get("/standard/1") + assert response.status_code == 200 + + # Make the requests concurrently and wait until they complete + start_time = datetime.now() + tasks = [asyncio.create_task(make_requests()) for _ in range(5)] + for task in tasks: + await task + print("Standard Requests: Took {delta}ms".format(delta=(datetime.now() - start_time).microseconds / 1000)) + + # View the metrics + metrics = await client.get("/metrics") + print("Standard Metrics:", metrics.json())