diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 23043f1..c51db90 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -4,17 +4,20 @@ jobs: test: name: Test - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: - python-version: [ "3.8", "3.9", "3.10", "3.11" ] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13" ] steps: - name: Setup dependencies - uses: ExpressApp/github-actions-poetry@v0.3 + uses: ExpressApp/github-actions-poetry@v0.4 with: python-version: ${{ matrix.python-version }} - poetry-version: "1.3.2" + poetry-version: "2.1.4" + + - name: Install dependencies with fastapi + run: poetry install --extras fastapi_utils - name: Run tests run: | @@ -23,17 +26,20 @@ jobs: lint: name: Lint - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: - python-version: [ "3.8", "3.9", "3.10", "3.11" ] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13" ] steps: - name: Setup dependencies - uses: ExpressApp/github-actions-poetry@v0.3 + uses: ExpressApp/github-actions-poetry@v0.4 with: python-version: ${{ matrix.python-version }} - poetry-version: "1.3.2" + poetry-version: "2.1.4" + + - name: Install dependencies with fastapi + run: poetry install --extras fastapi_utils - name: Run lint run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index ed534ee..0d83c24 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -5,12 +5,12 @@ on: - "*.*.*" jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v1 - name: Assert tag is from master run: git branch -a --contains $(git describe --tags) | grep master - name: Build and publish to pypi - uses: JRubics/poetry-publish@v1.8 + uses: JRubics/poetry-publish@v2.1 with: pypi_token: ${{ secrets.PYPI_TOKEN }} diff --git a/README.md b/README.md index b18aaf3..9da9f90 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +from pybotx_smartapp_rpc.new_openapi.refactored_utils.support_methods import get_rpc_flat_models_from_routes + # BotX-SmartApp-RPC Библиотека, позволяющая писать смартаппы, используя [наш JSONRPC-like протокол](https://ccsteam.atlassian.net/wiki/spaces/EI/pages/193167368/SmartApp+RPC) @@ -185,78 +187,38 @@ smartapp = SmartAppRPC(..., exception_handlers={KeyError: key_error_handler}) ### Swagger documentation Можно подключить rpc роутеры к авто генерируемой документации FastAPI и использовать документацию в Swagger. Для этого необходимо переопределить функцию для генерации -OpenAPI схемы: +OpenAPI схемы. + +Для этого необходимо: +1. Установить данную библиотеку с подключенной опцией +["fastapi_utils"]: +``` +pybotx-smartapp-rpc = {version, extras = ["fastapi_utils"]} +``` +2. При создании приложения подключить метод из библиотеки: ```python from fastapi import FastAPI - +from pybotx_smartapp_rpc.fastapi_utils.custom_openapi import rpc_openapi +from pybotx_smartapp_rpc.fastapi_utils.security import RPCAuth application = FastAPI() -def get_custom_openapi(): - return custom_openapi( - title="Smartapp API", +security = RPCAuth( + bot_id=bot_id_from_credentials +) + +def get_custom_openapi(): + return rpc_openapi( + title="Project Name", version="0.1.0", fastapi_routes=application.routes, - rpc_router=smartapp.router, + rpc_router=your_rpc_router, openapi_version="3.0.2", + security=security ) -application.openapi = get_custom_openapi -``` - -Пример функции `custom_openapi`: -```python -from fastapi.encoders import jsonable_encoder -from fastapi.openapi.models import OpenAPI -from fastapi.openapi.utils import get_openapi -from pybotx_smartapp_rpc import RPCRouter -from pybotx_smartapp_rpc.openapi_utils import * -from pydantic.schema import get_model_name_map -from starlette.routing import BaseRoute - - -def custom_openapi( - *, - title: str, - version: str, - fastapi_routes: Sequence[BaseRoute], - rpc_router: RPCRouter, - **kwargs: Any, -) -> Dict[str, Any]: - openapi_dict = get_openapi( - title=title, - version=version, - routes=fastapi_routes, - **kwargs, - ) - - paths: Dict[str, Dict[str, Any]] = {} - - flat_rpc_models = get_rpc_flat_models_from_routes(rpc_router) - rpc_model_name_map = get_model_name_map(flat_rpc_models) - rpc_definitions = get_rpc_model_definitions( - flat_models=flat_rpc_models, model_name_map=rpc_model_name_map - ) - - for method_name in rpc_router.rpc_methods.keys(): - if not rpc_router.rpc_methods[method_name].include_in_schema: - continue + application.openapi = get_custom_openapi # type: ignore - path = get_rpc_openapi_path( # type: ignore - method_name=method_name, - route=rpc_router.rpc_methods[method_name], - model_name_map=rpc_model_name_map, - ) - if path: - paths.setdefault(f"/{method_name}", {}).update(path) - - if rpc_definitions: - openapi_dict.setdefault("components", {}).setdefault("schemas", {}).update( - {k: rpc_definitions[k] for k in sorted(rpc_definitions)} - ) - - openapi_dict.setdefault("paths", {}).update(paths) - - return jsonable_encoder(OpenAPI(**openapi_dict), by_alias=True, exclude_none=True) ``` + ### Возможности RPC Swagger * Можно добавлять теги к запросам, анaлогично FastAPI. diff --git a/poetry.lock b/poetry.lock index 4debe0a..038d8d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,47 +1,41 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. - -[[package]] -name = "add-trailing-comma" -version = "2.5.1" -description = "Automatically add trailing commas to calls and literals" -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "add_trailing_comma-2.5.1-py2.py3-none-any.whl", hash = "sha256:603b4b55f76f85237f4c848e93e56a4c84ebf8c9e329daf44ce3e8111d68557b"}, - {file = "add_trailing_comma-2.5.1.tar.gz", hash = "sha256:f2a4afd41b96d6d34840455752c320c608942757ec6df3aa13b0b3c7effdd49e"}, -] - -[package.dependencies] -tokenize-rt = ">=3.0.1" +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "aiocsv" version = "1.3.2" description = "" -category = "main" optional = false python-versions = ">=3.8" files = [ {file = "aiocsv-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1996ac960c196aecc7d22e701c273a2676d13bf25575af78d4e515fc724ef20"}, {file = "aiocsv-1.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd688dbc1723f2b3a433e42041ceb9c9a8fe70f547d35b2da4ea31e4c78efc5"}, {file = "aiocsv-1.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f921828e386bb6945ed7d268e1524349ea506974ae35b9772542714f0ef3efd"}, + {file = "aiocsv-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c17dba00ac5a0ba0a3962902ebd60ed529a59440c957343175e815947ac7f114"}, {file = "aiocsv-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:198c905ec29897c347bf9b18eb410af13d7ac94a03d4b673e64eaa5f4557c913"}, {file = "aiocsv-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7c25ad8afbf79d28ec3320e608c7f38d3eff93e96ebbbd2430ae8fa0f6e7631b"}, {file = "aiocsv-1.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4004569bff39cb839a335b8f673a6496fd5b0b6e074c7adb7aee4a0c8379ea22"}, {file = "aiocsv-1.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e9c98f8d760add0b52274523baa4b81dde4a3c96f79222d3d4d6965bac9cdcbd"}, + {file = "aiocsv-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3dce5e3b18e24b2e06d93cbd8186eac2e6a385cac40bbdfa09d6110a7f48d40"}, {file = "aiocsv-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:9edb342b0d7dba94d8976f46ba5814b8d8704d67a45e1b8a6579ab0ba04309e7"}, + {file = "aiocsv-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5aa586564800df49280e0aa108acc855062ac5b9486bb052f0dd0c0051ea4f18"}, {file = "aiocsv-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:db943a463cb6828ba81bd7c083c6dd4c96edac4880b8638af81798d694405e26"}, {file = "aiocsv-1.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10780033a1ed3da825f2256449d177b7106b3c5a2d64bd683eab37f1fdee1e36"}, {file = "aiocsv-1.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8c7aee34ceff4eaa654f01acbdba648297f5f9532dc7a23fac62defec28e0fe5"}, + {file = "aiocsv-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f848e1cca7d22d8bd6480fa4c7338dc8be2abdd02e0b99f677b8a7af27e15767"}, {file = "aiocsv-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:59b0ea2d9e73539d4c1276467c4457acafa995717ea1b5340f3737f2cde2f71a"}, + {file = "aiocsv-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039dcf7bd684a98bf7c2218b8e7dc4abc951e1045dadd8813e992a1ba829ff"}, + {file = "aiocsv-1.3.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d8612392b7da7bff545b69202fb03a8e09381fff2d5c4d9594246d7375cd603"}, + {file = "aiocsv-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cee1577a381a44a18bcaed97c41f39b4400655de1a873f4e90b64af68e19dcd9"}, + {file = "aiocsv-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:0f0437f34ab7d1da86b30407653d635cf7de330681e746859b8c54aaac2c4574"}, {file = "aiocsv-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1c7d1700b8de16f25b24bfcebfc2b0817b29ce413f6961f08d5aa95bf00a6862"}, {file = "aiocsv-1.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa9629c8a1c07e9d02c7d80d84f021f7994fe30d021f13ac963e251b54724ef"}, {file = "aiocsv-1.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d125286f971e0038e8872f31b6f1cd6184b9c508445e6633f075d8b543b444bc"}, + {file = "aiocsv-1.3.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5fbfab48919aef505e2de38309f4808aa742dd23b834da6c670988e89b8b8577"}, {file = "aiocsv-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:b7220b4a6545abbbb6ab8fe7d4880aa8334f156b872b83641b898df2da9a6484"}, {file = "aiocsv-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfd2ef214b6d7944991f62ac593ad45bdaf0ed9f5741c8441ee7de148e512fe7"}, {file = "aiocsv-1.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c3e5a817b3489283cc1fd80f8ba56431d552dc9ea4e539c0069d8d56bf0fba7"}, {file = "aiocsv-1.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2ef14fa0839394ecc52274ea538b12b7b2e756eb0f514902a8fb391612161079"}, + {file = "aiocsv-1.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:188fc074ee8f72f1bab61c4838c36a354b15abd9c224285e7c60265c590fc87b"}, {file = "aiocsv-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:17341fa3b90414adda6cd8c79efc3c1a3f58a4dc72c2053c4532e82b61ef9f5e"}, {file = "aiocsv-1.3.2.tar.gz", hash = "sha256:806d93465c7808d58d3ff0d2bba270fb4d04b934be6a1e95d0834c50a510910e"}, ] @@ -51,191 +45,72 @@ typing_extensions = "*" [[package]] name = "aiofiles" -version = "23.2.1" +version = "24.1.0" description = "File support for asyncio." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, - {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, -] - -[[package]] -name = "anyio" -version = "4.4.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "astor" -version = "0.8.1" -description = "Read/rewrite/write Python ASTs" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -files = [ - {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, - {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, ] [[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -category = "dev" +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - [[package]] -name = "autoflake" -version = "1.7.8" -description = "Removes unused imports and unused variables" -category = "dev" +name = "anyio" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "autoflake-1.7.8-py3-none-any.whl", hash = "sha256:46373ef69b6714f5064c923bb28bd797c4f8a9497f557d87fc36665c6d956b39"}, - {file = "autoflake-1.7.8.tar.gz", hash = "sha256:e7e46372dee46fa1c97acf310d99d922b63d369718a270809d7c278d34a194cf"}, + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, ] [package.dependencies] -pyflakes = ">=1.1.0,<3" -tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} - -[[package]] -name = "bandit" -version = "1.7.9" -description = "Security oriented static analyser for python code." -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, - {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -baseline = ["GitPython (>=3.1.30)"] -sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] -toml = ["tomli (>=1.1.0)"] -yaml = ["PyYAML"] +trio = ["trio (>=0.26.1)"] [[package]] -name = "black" -version = "22.3.0" -description = "The uncompromising code formatter." -category = "dev" +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." optional = false -python-versions = ">=3.6.2" +python-versions = "<3.11,>=3.8" files = [ - {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, - {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, - {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, - {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, - {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, - {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, - {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, - {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, - {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, - {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, - {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, - {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, - {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, - {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, - {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, - {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, - {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, - {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, - {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, ] -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -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 = "certifi" -version = "2024.7.4" +version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -category = "dev" 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"}, + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -245,64 +120,99 @@ files = [ [[package]] name = "coverage" -version = "7.5.4" +version = "7.10.4" description = "Code coverage measurement for Python" -category = "dev" optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, - {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, - {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, - {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, - {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, - {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, - {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, - {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, - {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, - {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, - {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, - {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, - {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, - {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, - {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, - {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, - {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, - {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, - {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, - {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, - {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, - {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, - {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, - {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, - {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, - {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, - {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, - {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, +python-versions = ">=3.9" +files = [ + {file = "coverage-7.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d92d6edb0ccafd20c6fbf9891ca720b39c2a6a4b4a6f9cf323ca2c986f33e475"}, + {file = "coverage-7.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7202da14dc0236884fcc45665ffb2d79d4991a53fbdf152ab22f69f70923cc22"}, + {file = "coverage-7.10.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ada418633ae24ec8d0fcad5efe6fc7aa3c62497c6ed86589e57844ad04365674"}, + {file = "coverage-7.10.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b828e33eca6c3322adda3b5884456f98c435182a44917ded05005adfa1415500"}, + {file = "coverage-7.10.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:802793ba397afcfdbe9f91f89d65ae88b958d95edc8caf948e1f47d8b6b2b606"}, + {file = "coverage-7.10.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d0b23512338c54101d3bf7a1ab107d9d75abda1d5f69bc0887fd079253e4c27e"}, + {file = "coverage-7.10.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f36b7dcf72d06a8c5e2dd3aca02be2b1b5db5f86404627dff834396efce958f2"}, + {file = "coverage-7.10.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fce316c367a1dc2c411821365592eeb335ff1781956d87a0410eae248188ba51"}, + {file = "coverage-7.10.4-cp310-cp310-win32.whl", hash = "sha256:8c5dab29fc8070b3766b5fc85f8d89b19634584429a2da6d42da5edfadaf32ae"}, + {file = "coverage-7.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:4b0d114616f0fccb529a1817457d5fb52a10e106f86c5fb3b0bd0d45d0d69b93"}, + {file = "coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f"}, + {file = "coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88"}, + {file = "coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb"}, + {file = "coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9"}, + {file = "coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8"}, + {file = "coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2"}, + {file = "coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7"}, + {file = "coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0"}, + {file = "coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af"}, + {file = "coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52"}, + {file = "coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0"}, + {file = "coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79"}, + {file = "coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e"}, + {file = "coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e"}, + {file = "coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0"}, + {file = "coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62"}, + {file = "coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a"}, + {file = "coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23"}, + {file = "coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927"}, + {file = "coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a"}, + {file = "coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b"}, + {file = "coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a"}, + {file = "coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233"}, + {file = "coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169"}, + {file = "coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74"}, + {file = "coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef"}, + {file = "coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408"}, + {file = "coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd"}, + {file = "coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097"}, + {file = "coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690"}, + {file = "coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e"}, + {file = "coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2"}, + {file = "coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7"}, + {file = "coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84"}, + {file = "coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484"}, + {file = "coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9"}, + {file = "coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d"}, + {file = "coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc"}, + {file = "coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec"}, + {file = "coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9"}, + {file = "coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4"}, + {file = "coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c"}, + {file = "coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f"}, + {file = "coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2"}, + {file = "coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4"}, + {file = "coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6"}, + {file = "coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4"}, + {file = "coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c"}, + {file = "coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e"}, + {file = "coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76"}, + {file = "coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818"}, + {file = "coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf"}, + {file = "coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd"}, + {file = "coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a"}, + {file = "coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38"}, + {file = "coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6"}, + {file = "coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508"}, + {file = "coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f"}, + {file = "coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214"}, + {file = "coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1"}, + {file = "coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec"}, + {file = "coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d"}, + {file = "coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3"}, + {file = "coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd"}, + {file = "coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd"}, + {file = "coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c"}, + {file = "coverage-7.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:48fd4d52600c2a9d5622e52dfae674a7845c5e1dceaf68b88c99feb511fbcfd6"}, + {file = "coverage-7.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:56217b470d09d69e6b7dcae38200f95e389a77db801cb129101697a4553b18b6"}, + {file = "coverage-7.10.4-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:44ac3f21a6e28c5ff7f7a47bca5f87885f6a1e623e637899125ba47acd87334d"}, + {file = "coverage-7.10.4-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3387739d72c84d17b4d2f7348749cac2e6700e7152026912b60998ee9a40066b"}, + {file = "coverage-7.10.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f111ff20d9a6348e0125be892608e33408dd268f73b020940dfa8511ad05503"}, + {file = "coverage-7.10.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01a852f0a9859734b018a3f483cc962d0b381d48d350b1a0c47d618c73a0c398"}, + {file = "coverage-7.10.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:225111dd06759ba4e37cee4c0b4f3df2b15c879e9e3c37bf986389300b9917c3"}, + {file = "coverage-7.10.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2178d4183bd1ba608f0bb12e71e55838ba1b7dbb730264f8b08de9f8ef0c27d0"}, + {file = "coverage-7.10.4-cp39-cp39-win32.whl", hash = "sha256:93d175fe81913aee7a6ea430abbdf2a79f1d9fd451610e12e334e4fe3264f563"}, + {file = "coverage-7.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:2221a823404bb941c7721cf0ef55ac6ee5c25d905beb60c0bba5e5e85415d353"}, + {file = "coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302"}, + {file = "coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27"}, ] [package.dependencies] @@ -312,388 +222,150 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 toml = ["tomli"] [[package]] -name = "darglint" -version = "1.8.1" -description = "A utility for ensuring Google-style docstrings stay up to date with the source code." -category = "dev" +name = "deepdiff" +version = "8.6.0" +description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.9" files = [ - {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, - {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, + {file = "deepdiff-8.6.0-py3-none-any.whl", hash = "sha256:db80677a434ac1f84147fd1598e93f1beb06d467e107af45fcf77cf8a681169f"}, + {file = "deepdiff-8.6.0.tar.gz", hash = "sha256:6197216c2d777c3106a9989055c230e25848e599b26dcbcdc66226bd8d7fe901"}, ] -[[package]] -name = "docutils" -version = "0.20.1" -description = "Docutils -- Python Documentation Utilities" -category = "dev" -optional = false -python-versions = "*" -files = [] +[package.dependencies] +orderly-set = ">=5.4.1,<6" -[[package]] -name = "eradicate" -version = "2.3.0" -description = "Removes commented-out code." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, - {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, -] +[package.extras] +cli = ["click (>=8.1.0,<8.2.0)", "pyyaml (>=6.0.0,<6.1.0)"] +coverage = ["coverage (>=7.6.0,<7.7.0)"] +dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)", "jsonpickle (>=4.0.0,<4.1.0)", "nox (==2025.5.1)", "numpy (>=2.0,<3.0)", "numpy (>=2.2.0,<2.3.0)", "orjson (>=3.10.0,<3.11.0)", "pandas (>=2.2.0,<2.3.0)", "polars (>=1.21.0,<1.22.0)", "python-dateutil (>=2.9.0,<2.10.0)", "tomli (>=2.2.0,<2.3.0)", "tomli-w (>=1.2.0,<1.3.0)", "uuid6 (==2025.0.1)"] +docs = ["Sphinx (>=6.2.0,<6.3.0)", "sphinx-sitemap (>=2.6.0,<2.7.0)", "sphinxemoji (>=0.3.0,<0.4.0)"] +optimize = ["orjson"] +static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"] +test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] [package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" - -[[package]] -name = "flake8-bandit" -version = "3.0.0" -description = "Automated security testing with bandit and flake8." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "flake8_bandit-3.0.0-py2.py3-none-any.whl", hash = "sha256:61b617f4f7cdaa0e2b1e6bf7b68afb2b619a227bb3e3ae00dd36c213bd17900a"}, - {file = "flake8_bandit-3.0.0.tar.gz", hash = "sha256:54d19427e6a8d50322a7b02e1841c0a7c22d856975f3459803320e0e18e2d6a1"}, -] - -[package.dependencies] -bandit = ">=1.7.3" -flake8 = "*" -flake8-polyfill = "*" -pycodestyle = "*" - -[[package]] -name = "flake8-broken-line" -version = "0.4.0" -description = "Flake8 plugin to forbid backslashes for line breaks" -category = "dev" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "flake8-broken-line-0.4.0.tar.gz", hash = "sha256:771aab5aa0997666796fed249d0e48e6c01cdfeca8c95521eea28a38b7ced4c7"}, - {file = "flake8_broken_line-0.4.0-py3-none-any.whl", hash = "sha256:e9c522856862239a2c7ef2c1de0276fa598572aa864bd4e9c7efc2a827538515"}, -] - -[package.dependencies] -flake8 = ">=3.5,<5" - -[[package]] -name = "flake8-bugbear" -version = "22.12.6" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-bugbear-22.12.6.tar.gz", hash = "sha256:4cdb2c06e229971104443ae293e75e64c6107798229202fbe4f4091427a30ac0"}, - {file = "flake8_bugbear-22.12.6-py3-none-any.whl", hash = "sha256:b69a510634f8a9c298dfda2b18a8036455e6b19ecac4fe582e4d7a0abfa50a30"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -flake8 = ">=3.0.0" +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"] - -[[package]] -name = "flake8-commas" -version = "2.1.0" -description = "Flake8 lint for trailing commas." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-commas-2.1.0.tar.gz", hash = "sha256:940441ab8ee544df564ae3b3f49f20462d75d5c7cac2463e0b27436e2050f263"}, - {file = "flake8_commas-2.1.0-py2.py3-none-any.whl", hash = "sha256:ebb96c31e01d0ef1d0685a21f3f0e2f8153a0381430e748bf0bbbb5d5b453d54"}, -] - -[package.dependencies] -flake8 = ">=2" +test = ["pytest (>=6)"] [[package]] -name = "flake8-comprehensions" -version = "3.15.0" -description = "A flake8 plugin to help you write better list/set/dict comprehensions." -category = "dev" -optional = false +name = "fastapi" +version = "0.115.14" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = true python-versions = ">=3.8" files = [ - {file = "flake8_comprehensions-3.15.0-py3-none-any.whl", hash = "sha256:b7e027bbb52be2ceb779ee12484cdeef52b0ad3c1fcb8846292bdb86d3034681"}, - {file = "flake8_comprehensions-3.15.0.tar.gz", hash = "sha256:923c22603e0310376a6b55b03efebdc09753c69f2d977755cba8bb73458a5d4d"}, -] - -[package.dependencies] -flake8 = ">=3,<3.2 || >3.2" - -[[package]] -name = "flake8-debugger" -version = "4.1.2" -description = "ipdb/pdb statement checker plugin for flake8" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"}, - {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"}, + {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, + {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, ] [package.dependencies] -flake8 = ">=3.0" -pycodestyle = "*" - -[[package]] -name = "flake8-docstrings" -version = "1.7.0" -description = "Extension for flake8 which uses pydocstyle to check docstrings" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, - {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, -] - -[package.dependencies] -flake8 = ">=3" -pydocstyle = ">=2.1" - -[[package]] -name = "flake8-eradicate" -version = "1.4.0" -description = "Flake8 plugin to find commented out code" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "flake8-eradicate-1.4.0.tar.gz", hash = "sha256:3088cfd6717d1c9c6c3ac45ef2e5f5b6c7267f7504d5a74b781500e95cb9c7e1"}, - {file = "flake8_eradicate-1.4.0-py3-none-any.whl", hash = "sha256:e3bbd0871be358e908053c1ab728903c114f062ba596b4d40c852fd18f473d56"}, -] - -[package.dependencies] -attrs = "*" -eradicate = ">=2.0,<3.0" -flake8 = ">=3.5,<6" - -[[package]] -name = "flake8-isort" -version = "4.2.0" -description = "flake8 plugin that integrates isort ." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-isort-4.2.0.tar.gz", hash = "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0"}, - {file = "flake8_isort-4.2.0-py3-none-any.whl", hash = "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf"}, -] - -[package.dependencies] -flake8 = ">=3.2.1,<6" -isort = ">=4.3.5,<6" +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.40.0,<0.47.0" +typing-extensions = ">=4.8.0" [package.extras] -test = ["pytest-cov"] - -[[package]] -name = "flake8-polyfill" -version = "1.0.2" -description = "Polyfill package for Flake8 plugins" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, - {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, -] - -[package.dependencies] -flake8 = "*" - -[[package]] -name = "flake8-quotes" -version = "3.4.0" -description = "Flake8 lint for quotes." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-quotes-3.4.0.tar.gz", hash = "sha256:aad8492fb710a2d3eabe68c5f86a1428de650c8484127e14c43d0504ba30276c"}, -] - -[package.dependencies] -flake8 = "*" -setuptools = "*" - -[[package]] -name = "flake8-rst-docstrings" -version = "0.2.7" -description = "Python docstring reStructuredText (RST) validator" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-rst-docstrings-0.2.7.tar.gz", hash = "sha256:2740067ab9237559dd45a3434d8c987792c7b259ca563621a3b95efe201f5382"}, - {file = "flake8_rst_docstrings-0.2.7-py3-none-any.whl", hash = "sha256:5d56075dce360bcc9c6775bfe7cb431aa395de600ca7e8d40580a28d50b2a803"}, -] - -[package.dependencies] -flake8 = ">=3.0.0" -pygments = "*" -restructuredtext-lint = "*" - -[[package]] -name = "flake8-string-format" -version = "0.3.0" -description = "string format checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, - {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, -] - -[package.dependencies] -flake8 = "*" +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "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)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.9" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] -trio = ["trio (>=0.22.0,<0.23.0)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.25.2" +version = "0.28.1" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, - {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] anyio = "*" certifi = "*" -httpcore = ">=1.0.0,<2.0.0" +httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" -category = "dev" -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 = "isort" -version = "5.10.1" -description = "A Python utility / library to sort Python imports." -category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.8" files = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] -[package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - [[package]] name = "loguru" version = "0.6.0" description = "Python logging made (stupidly) simple" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -709,124 +381,103 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] [[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -category = "dev" +name = "mypy" +version = "1.16.1" +description = "Optional static typing for Python" 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" +python-versions = ">=3.9" +files = [ + {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, + {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, + {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, + {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, + {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, + {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, + {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, + {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, + {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, + {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, + {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, + {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, + {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, + {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, + {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, + {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, + {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, + {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, + {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, + {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, + {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, + {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, + {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, + {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, + {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, + {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, + {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, + {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, + {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, + {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, + {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, + {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.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 = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -category = "dev" +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] -name = "mypy" -version = "0.910" -description = "Optional static typing for Python" -category = "dev" +name = "orderly-set" +version = "5.5.0" +description = "Orderly set" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, - {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, - {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, - {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, - {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, - {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, - {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, - {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, - {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, - {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, - {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, - {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, - {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, - {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, - {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, - {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, - {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, - {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, - {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, - {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, - {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, - {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, - {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, + {file = "orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7"}, + {file = "orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce"}, ] -[package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -toml = "*" -typing-extensions = ">=3.7.4" - [package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<1.5.0)"] - -[[package]] -name = "mypy-extensions" -version = "0.4.4" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "main" -optional = false -python-versions = ">=2.7" -files = [ - {file = "mypy_extensions-0.4.4.tar.gz", hash = "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd"}, -] +coverage = ["coverage (>=7.6.0,<7.7.0)"] +dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)"] +optimize = ["orjson"] +static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)"] +test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] [[package]] name = "packaging" -version = "24.1" +version = "25.0" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -834,201 +485,183 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] -[[package]] -name = "pbr" -version = "6.0.0" -description = "Python Build Reasonableness" -category = "dev" -optional = false -python-versions = ">=2.6" -files = [ - {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, - {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, -] - -[[package]] -name = "pep8-naming" -version = "0.12.1" -description = "Check PEP-8 naming conventions, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"}, - {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, -] - -[package.dependencies] -flake8 = ">=3.9.1" -flake8-polyfill = ">=1.0.2,<2" - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pybotx" -version = "0.69.0" +version = "0.76.0a1" description = "A python library for interacting with eXpress BotX API" -category = "main" optional = false -python-versions = "<3.13,>=3.8" +python-versions = "<3.14,>=3.9" files = [ - {file = "pybotx-0.69.0-py3-none-any.whl", hash = "sha256:993a8c3c176d7a18a9eadc1c5eda76abb3bcc2fbd7154d51fe84b678f122cf10"}, - {file = "pybotx-0.69.0.tar.gz", hash = "sha256:6aed8a4db7fef5a50e5e05db531c48b0dc7585dd844ab4df63b933b14528919f"}, + {file = "pybotx-0.76.0a1-py3-none-any.whl", hash = "sha256:2051da3c627e254458d995127335f4781a9e382338fb9f7fc4945085c15390fb"}, + {file = "pybotx-0.76.0a1.tar.gz", hash = "sha256:fd07877244eeea7926fbeb35276ff64817a776393349a6ee936eb25073e6ed70"}, ] [package.dependencies] -aiocsv = ">=1.2.3,<1.4.0" -aiofiles = ">=0.7.0,<24.0.0" -httpcore = ">=1.0.0,<1.0.3" -httpx = ">=0.25.0,<0.26.0" +aiocsv = ">=1.2.3,<=1.4.0" +aiofiles = ">=0.7.0,<=24.1.0" +httpcore = "1.0.9" +httpx = ">=0.28.0,<0.29.0" loguru = ">=0.6.0,<0.7.0" -mypy-extensions = ">=0.2.0,<0.5.0" -pydantic = ">=1.6.0,<1.11.0" +mypy-extensions = ">=0.2.0,<=1.1.0" +pydantic = ">=2.8.2,<3.0" pyjwt = ">=2.0.0,<3.0.0" -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] - [[package]] name = "pydantic" -version = "1.10.17" -description = "Data validation and settings management using python type hints" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, - {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, - {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, - {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, - {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, - {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, - {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, - {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, - {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, - {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, - {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, -] - -[package.dependencies] -typing-extensions = ">=4.2.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -category = "dev" +version = "2.11.7" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] -snowballstemmer = ">=2.2.0" +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] -toml = ["tomli (>=1.2.3)"] - -[[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -1036,198 +669,114 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.8.0" +version = "2.10.1" description = "JSON Web Token implementation in Python" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, - {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] [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"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pytest" -version = "7.4.4" +version = "8.4.1" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [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\""} +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", 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"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.18.3" +version = "1.1.0" description = "Pytest support for asyncio" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, - {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, - {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, + {file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"}, + {file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"}, ] [package.dependencies] -pytest = ">=6.1.0" +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<9" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.10\""} [package.extras] -testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "6.2.1" description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -category = "dev" -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-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {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-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {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-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {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-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {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-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {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 = "restructuredtext-lint" -version = "1.4.0" -description = "reStructuredText linter" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"}, -] - -[package.dependencies] -docutils = ">=0.11,<1.0" - -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "dev" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.9" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [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\""} +coverage = {version = ">=7.5", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=6.2.5" [package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] -name = "setuptools" -version = "70.3.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" +name = "ruff" +version = "0.12.0" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, - {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, + {file = "ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848"}, + {file = "ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6"}, + {file = "ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a"}, + {file = "ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb"}, + {file = "ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0"}, + {file = "ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b"}, + {file = "ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c"}, ] -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1236,129 +785,107 @@ files = [ ] [[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" -optional = false -python-versions = "*" +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = true +python-versions = ">=3.9" files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "stevedore" -version = "5.2.0" -description = "Manage dynamic plugins for Python applications" -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, - {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, ] [package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" - -[[package]] -name = "tokenize-rt" -version = "5.2.0" -description = "A wrapper around the stdlib `tokenize` which roundtrips." -category = "dev" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, - {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, -] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -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.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] [[package]] -name = "wemake-python-styleguide" -version = "0.16.1" -description = "The strictest and most opinionated python linter ever" -category = "dev" +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.9" files = [ - {file = "wemake-python-styleguide-0.16.1.tar.gz", hash = "sha256:4fcd78dd55732679b5fc8bc37fd7e04bbaa5cdc1b1a829ad265e8f6b0d853cf6"}, - {file = "wemake_python_styleguide-0.16.1-py3-none-any.whl", hash = "sha256:202c22ecfee1f5caf0555048602cd52f2435cd57903e6b0cd46b5aaa3f652140"}, + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, ] [package.dependencies] -astor = ">=0.8,<0.9" -attrs = "*" -darglint = ">=1.2,<2.0" -flake8 = ">=3.7,<5" -flake8-bandit = ">=2.1,<4" -flake8-broken-line = ">=0.3,<0.5" -flake8-bugbear = ">=20.1,<23.0" -flake8-commas = ">=2.0,<3.0" -flake8-comprehensions = ">=3.1,<4.0" -flake8-debugger = ">=4.0,<5.0" -flake8-docstrings = ">=1.3,<2.0" -flake8-eradicate = ">=1.0,<2.0" -flake8-isort = ">=4.0,<5.0" -flake8-quotes = ">=3.0,<4.0" -flake8-rst-docstrings = ">=0.2,<0.3" -flake8-string-format = ">=0.3,<0.4" -pep8-naming = ">=0.11,<0.13" -pygments = ">=2.4,<3.0" -typing_extensions = ">=3.6,<5.0" +typing-extensions = ">=4.12.0" [[package]] name = "win32-setctime" -version = "1.1.0" +version = "1.2.0" description = "A small Python utility to set file creation time on Windows" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, - {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, ] [package.extras] dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] +[extras] +fastapi-utils = ["fastapi"] + [metadata] lock-version = "2.0" -python-versions = ">=3.8,<3.12" -content-hash = "f45a38dbe2e2a9208427d76e48bb464d142aa2830bff9cd044343ca0c94d7e49" +python-versions = ">=3.9,<3.14" +content-hash = "5b6d55bb91483725142e08075e3510fd2c153be87a836d5756d4ac67825f07bb" diff --git a/pybotx_smartapp_rpc/empty_args.py b/pybotx_smartapp_rpc/empty_args.py index 085be29..c27995f 100644 --- a/pybotx_smartapp_rpc/empty_args.py +++ b/pybotx_smartapp_rpc/empty_args.py @@ -2,4 +2,4 @@ class EmptyArgs(RPCArgsBaseModel): - pass # noqa: WPS420, WPS604 + pass diff --git a/pybotx_smartapp_rpc/exceptions.py b/pybotx_smartapp_rpc/exceptions.py index f00e997..ba0daf3 100644 --- a/pybotx_smartapp_rpc/exceptions.py +++ b/pybotx_smartapp_rpc/exceptions.py @@ -3,11 +3,10 @@ from pybotx_smartapp_rpc.models.errors import RPCError -class BaseRPCErrorExc(Exception): - ... # noqa: WPS428, WPS604 +class BaseRPCErrorExc(Exception): ... # noqa: N818 -class RPCErrorExc(BaseRPCErrorExc): +class RPCErrorExc(BaseRPCErrorExc): # noqa: N818 def __init__(self, error_or_errors: Union[RPCError, List[RPCError]]): if isinstance(error_or_errors, RPCError): self.errors = [error_or_errors] diff --git a/pybotx_smartapp_rpc/fastapi_utils/__init__.py b/pybotx_smartapp_rpc/fastapi_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pybotx_smartapp_rpc/fastapi_utils/custom_openapi.py b/pybotx_smartapp_rpc/fastapi_utils/custom_openapi.py new file mode 100644 index 0000000..0ef25ac --- /dev/null +++ b/pybotx_smartapp_rpc/fastapi_utils/custom_openapi.py @@ -0,0 +1,62 @@ +from typing import Any, Sequence + +from fastapi.encoders import jsonable_encoder +from fastapi.openapi.models import OpenAPI +from fastapi.openapi.utils import get_openapi +from starlette.routing import BaseRoute + +from pybotx_smartapp_rpc import RPCRouter +from pybotx_smartapp_rpc.fastapi_utils.security import ( + RPCAuth, + get_openapi_security_definitions, +) +from pybotx_smartapp_rpc.openapi.openapi import update_fastapi_paths_by_rpc_router + + +def rpc_openapi( + *, + title: str, + version: str, + fastapi_routes: Sequence[BaseRoute], + rpc_router: RPCRouter, + security: RPCAuth, + **kwargs: Any, +) -> dict[str, Any]: + """ + Generates an OpenAPI dictionary for FastAPI routes combined with RPC routes. + + This function integrates a traditional FastAPI route setup with an RPC-based routing + mechanism, generating a single OpenAPI schema definition. + It also constructs security definitions and integrates them into the resulting + OpenAPI schema. + + Args: + title: Title of the OpenAPI schema document. + version: Version of the API described by the schema. + fastapi_routes: List of FastAPI routes to include in the schema. + rpc_router: Router containing RPC route definitions. + security: Security component describing authentication mechanisms. + **kwargs: Additional keyword arguments to pass to the OpenAPI generation. + + Returns: + dict[str, Any]: A dictionary representing the OpenAPI schema. + + Raises: + ValueError: If the given arguments are invalid. + """ + openapi_dict = get_openapi( + title=title, + version=version, + routes=fastapi_routes, + **kwargs, + ) + + security_definitions, operation_security = get_openapi_security_definitions( + security_component=security + ) + + update_fastapi_paths_by_rpc_router( + openapi_dict, rpc_router, security_definitions, operation_security + ) + + return jsonable_encoder(OpenAPI(**openapi_dict), by_alias=True, exclude_none=True) diff --git a/pybotx_smartapp_rpc/fastapi_utils/security.py b/pybotx_smartapp_rpc/fastapi_utils/security.py new file mode 100644 index 0000000..9c48ff9 --- /dev/null +++ b/pybotx_smartapp_rpc/fastapi_utils/security.py @@ -0,0 +1,103 @@ +import re +from typing import Any +from uuid import UUID, uuid4 + +from fastapi import HTTPException +from fastapi.encoders import jsonable_encoder +from fastapi.security import APIKeyHeader +from fastapi.security.base import SecurityBase +from pydantic import BaseModel, ValidationError +from starlette.requests import Request +from starlette.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN + + +def get_openapi_security_definitions( + security_component: SecurityBase, +) -> tuple[dict[str, Any], dict[str, Any]]: + """Get the security definitions and operation security for a security component.""" + security_definition = jsonable_encoder( + security_component.model, + by_alias=True, + exclude_none=True, + ) + security_name = security_component.scheme_name + security_definitions = {security_name: security_definition} + operation_security = {security_name: []} # type: ignore + return security_definitions, operation_security + + +DOCS = """Установка параметров для выполнение RPC методов. + +* `bot_id` - huid бота. Необязательное поле. +* `sender_huid` - huid пользователя. Необязательное поле. +* `sender_udid` - udid пользователя. Необязательное поле. +* `chat_id` - id чата. Необязательное поле. + +**Example**: `bot_id=UUID&sender_huid=UUID&sender_udid=UUID&chat_id=UUID`""" + + +class RPCAuthConfig(BaseModel): + bot_id: UUID + sender_huid: UUID = uuid4() + sender_udid: UUID = uuid4() + chat_id: UUID = uuid4() + + +class RPCAuth(APIKeyHeader): + """ + Handles RPC Authentication implementation via custom API key headers. + + This class extends `APIKeyHeader` to enable a mechanism for validating and + parsing RPC API authentication headers. It extracts authentication parameters + from the provided API key in the request headers, validates their format, and + returns a properly configured instance of `RPCAuthConfig`. The `bot_id` is + included as a mandatory attribute for any authentication config. + + Attributes: + PATTERN: str + Regex pattern used to extract key-value pairs from the API key header. + + Methods: + __call__(request: Request) -> RPCAuthConfig + Parses and validates the API key from the incoming request, returning + the RPC authentication config or raising an appropriate HTTP exception. + """ + + PATTERN = "([^?=&]+)=([^&]*)" + + def __init__( + self, + *, + bot_id: UUID, + scheme_name: str = "RPC Auth", + name: str = "X-RPC-AUTH", + description: str = DOCS, + **kwargs: Any, + ) -> None: + self.bot_id = bot_id + super().__init__( + scheme_name=scheme_name, name=name, description=description, **kwargs + ) + + async def __call__(self, request: Request) -> RPCAuthConfig: # type: ignore + api_key = request.headers.get(self.model.name) + if not api_key: + return RPCAuthConfig(bot_id=self.bot_id) + + params = re.findall(self.PATTERN, api_key) + if not params: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Invalid RPC Auth format" + ) + try: + params_dict = dict(params) + if "bot_id" not in params_dict: + params_dict["bot_id"] = self.bot_id + + config = RPCAuthConfig(**params_dict) + except ValidationError as ex: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, detail=str(ex) + ) from None + + return config diff --git a/pybotx_smartapp_rpc/middlewares/empty_args_middleware.py b/pybotx_smartapp_rpc/middlewares/empty_args_middleware.py index a56f9b8..19be612 100644 --- a/pybotx_smartapp_rpc/middlewares/empty_args_middleware.py +++ b/pybotx_smartapp_rpc/middlewares/empty_args_middleware.py @@ -16,6 +16,17 @@ async def empty_args_middleware( rpc_arguments: RPCArgsBaseModel, call_next: Handler, ) -> RPCResponse: + """ + Middleware function to handle RPC arguments and route the call accordingly based on + whether the arguments are of type `EmptyArgs` or not. + + :param smartapp: An instance of `SmartApp` representing. + :param rpc_arguments: An instance of `RPCArgsBaseModel` containing the RPC arguments + to be validated and passed to the next handler. + :param call_next: A handler function that takes either `smartapp` alone or both + `smartapp` and `rpc_arguments`, determined by the type of the RPC arguments. + :return: An `RPCResponse` object resulting from calling the appropriate handler. + """ if isinstance(rpc_arguments, EmptyArgs): call_next = cast(HandlerWithoutArgs, call_next) return await call_next(smartapp) diff --git a/pybotx_smartapp_rpc/middlewares/exception_middleware.py b/pybotx_smartapp_rpc/middlewares/exception_middleware.py index 79486fa..ebe963d 100644 --- a/pybotx_smartapp_rpc/middlewares/exception_middleware.py +++ b/pybotx_smartapp_rpc/middlewares/exception_middleware.py @@ -12,6 +12,16 @@ class ExceptionMiddleware: + """ + Middleware for handling exceptions during the execution of a request. + + This class allows defining custom exception handlers for different types of + exceptions that might occur during the execution of a specific request. + + :ivar _exception_handlers: A dictionary mapping exception types to their + corresponding handlers. Each handler should be a coroutine function. + """ + def __init__( self, exception_handlers: Optional[ExceptionHandlerDict] = None, @@ -28,7 +38,7 @@ async def __call__( rpc_result = await call_next(smartapp, rpc_arguments) except Exception as exc: exception_handler = self._get_exception_handler(exc) - try: # noqa: WPS505 + try: return await exception_handler(exc, smartapp) except Exception as error_handler_exc: return await default_exception_handler(error_handler_exc, smartapp) diff --git a/pybotx_smartapp_rpc/models/method.py b/pybotx_smartapp_rpc/models/method.py index 26580bf..d692ee2 100644 --- a/pybotx_smartapp_rpc/models/method.py +++ b/pybotx_smartapp_rpc/models/method.py @@ -3,8 +3,7 @@ from functools import partial from typing import Dict, List, Optional, Union -from pydantic.fields import ModelField - +from pybotx_smartapp_rpc.models.model_field import ModelField from pybotx_smartapp_rpc.smartapp import SmartApp from pybotx_smartapp_rpc.typing import ( Handler, @@ -17,6 +16,30 @@ @dataclass class RPCMethod: + """ + Represents a Remote Procedure Call (RPC) method and its associated behavior. + + This class encapsulates the details and execution logic for an RPC method, + including its handler, middlewares, response structure, and configurations. + It is responsible for assembling the middleware stack and invoking the RPC handler + with the provided arguments. + + :ivar handler: The main handler function for the RPC method. + :ivar middlewares: A list of middleware to be applied to the RPC method. + :ivar response_field: The field defining the structure of the response returned by + the RPC method. + :ivar arguments_field: Optional field defining the structure of input arguments + for the RPC method. + :ivar tags: Tags associated with the RPC method, used for classification or + documentation purposes. + :ivar errors: A dictionary mapping error codes to error details relevant + to the RPC method. + :ivar errors_models: A dictionary mapping error codes to model descriptions + for error handling in the RPC method. + :ivar include_in_schema: Indicates whether the RPC method should be + included in the API schema. + """ + handler: Handler middlewares: List[Middleware] response_field: ModelField @@ -36,7 +59,7 @@ async def __call__( # then stack will be m1(m2(m3(m4(handler())))) handler: HandlerWithArgs = self.handler # type: ignore for middleware in self.middlewares[::-1]: - part = partial(middleware, call_next=handler) + part = partial(middleware, call_next=handler) # type: ignore handler = part return await handler(smartapp, rpc_args) diff --git a/pybotx_smartapp_rpc/models/model_field.py b/pybotx_smartapp_rpc/models/model_field.py new file mode 100644 index 0000000..098aa2c --- /dev/null +++ b/pybotx_smartapp_rpc/models/model_field.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass +from typing import Any + +from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined as Undefined + + +@dataclass +class ModelField: + """ + Represents a model field with metadata and utilities for managing its properties. + + The ModelField class encapsulates information about a model's field, including its + name, type hint, default value, and whether it is required. It provides methods + and properties to query and manage these attributes effectively. + + :ivar field_info: Metadata and information about the field. + :ivar name: The name of the field. + """ + + field_info: FieldInfo + name: str + + @property + def required(self) -> bool: + return self.field_info.is_required() + + @property + def default(self) -> Any: + return self.get_default() + + @property + def type_(self) -> Any: + return self.field_info.annotation + + def get_default(self) -> Any: + if self.field_info.is_required(): + return Undefined + return self.field_info.get_default(call_default_factory=True) diff --git a/pybotx_smartapp_rpc/models/request.py b/pybotx_smartapp_rpc/models/request.py index 063562a..4e04e37 100644 --- a/pybotx_smartapp_rpc/models/request.py +++ b/pybotx_smartapp_rpc/models/request.py @@ -4,8 +4,7 @@ class RPCArgsBaseModel(BaseModel): - class Config: - allow_population_by_field_name = True + model_config = {"populate_by_name": True} class RPCRequest(BaseModel): diff --git a/pybotx_smartapp_rpc/models/responses.py b/pybotx_smartapp_rpc/models/responses.py index c7bd3f7..856732a 100644 --- a/pybotx_smartapp_rpc/models/responses.py +++ b/pybotx_smartapp_rpc/models/responses.py @@ -2,8 +2,7 @@ from typing import Any, Dict, Generic, List, TypeVar, Union from pybotx import File -from pydantic import BaseModel -from pydantic.error_wrappers import ValidationError +from pydantic import BaseModel, ValidationError from pybotx_smartapp_rpc.models.errors import RPCError @@ -15,8 +14,7 @@ class RPCResponseBaseModel(BaseModel): - class Config: - allow_population_by_field_name = True + model_config = {"populate_by_name": True} @dataclass @@ -32,11 +30,11 @@ def jsonable_dict(self) -> Dict[str, Any]: "result": self.jsonable_result(), } - def jsonable_result(self) -> JsonableResultType: + def jsonable_result(self) -> _JsonableResultType: if isinstance(self.result, BaseModel): - return self.result.dict(by_alias=True) # type: ignore + return self.result.model_dump(by_alias=True) - return self.result # type: ignore + return self.result @dataclass @@ -53,7 +51,7 @@ def jsonable_dict(self) -> Dict[str, Any]: } def jsonable_errors(self) -> List[Dict[str, Any]]: - return [error.dict() for error in self.errors] + return [error.model_dump() for error in self.errors] def build_invalid_rpc_request_error_response( diff --git a/pybotx_smartapp_rpc/openapi/__init__.py b/pybotx_smartapp_rpc/openapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pybotx_smartapp_rpc/openapi/openapi.py b/pybotx_smartapp_rpc/openapi/openapi.py new file mode 100644 index 0000000..b1e5d48 --- /dev/null +++ b/pybotx_smartapp_rpc/openapi/openapi.py @@ -0,0 +1,226 @@ +from enum import Enum +from typing import Any, Dict, List, Optional, Set, Type, Union + +from pydantic import BaseModel, TypeAdapter + +from pybotx_smartapp_rpc import RPCRouter +from pybotx_smartapp_rpc.models.method import RPCMethod +from pybotx_smartapp_rpc.models.model_field import ModelField +from pybotx_smartapp_rpc.openapi.utils import ( + deep_dict_update, + get_flat_models_from_fields, + get_model_name_map, + get_schema_or_ref, +) + +REF_PREFIX = "#/components/schemas/" +ModelNameMap: type[dict[type[Union[BaseModel, Enum]], str]] = Dict[ + Union[Type[BaseModel], Type[Enum]], str +] + + +def update_fastapi_paths_by_rpc_router( + openapi_dict: dict[str, Any], + rpc_router: RPCRouter, + security_definitions: Optional[dict[str, Any]] = None, + operation_security: Optional[dict[str, Any]] = None, +) -> None: + """ + Updates the OpenAPI dictionary by adding paths and components related to the + provided RPCRouter and its configured methods. + Additionally, updates security definitions and operation-specific security + configurations if provided. + + :param openapi_dict: The base OpenAPI dictionary to be updated with the RPC + router paths. + :param rpc_router: The RPCRouter instance containing the RPC methods to include + in the schema. + :param security_definitions: Optional dictionary defining security schemes to add + to the OpenAPI specification. + :param operation_security: Optional dictionary specifying operation-level security + configurations for RPC methods in the OpenAPI schema. + :return: Nothing. Modifies the `openapi_dict` in place. + """ + if security_definitions is not None: + openapi_dict.setdefault("components", {}).setdefault( + "securitySchemes", {} + ).update(security_definitions) + + paths: Dict[str, Dict[str, Any]] = {} + + flat_rpc_models = get_rpc_flat_models_from_routes(rpc_router) + rpc_model_name_map = get_model_name_map(flat_rpc_models) + rpc_definitions = get_rpc_model_definitions( + flat_models=flat_rpc_models, model_name_map=rpc_model_name_map + ) + + for method_name, method in rpc_router.rpc_methods.items(): + if not method.include_in_schema: + continue + + if path := get_rpc_openapi_path( + method_name=method_name, + route=method, + model_name_map=rpc_model_name_map, + security_scheme=operation_security, + ): + paths.setdefault(f"/{method_name}", {}).update(path) + + if rpc_definitions: + openapi_dict.setdefault("components", {}).setdefault("schemas", {}).update( + {k: rpc_definitions[k] for k in sorted(rpc_definitions)} + ) + + openapi_dict.setdefault("paths", {}).update(paths) + + +def get_rpc_model_definitions( + *, + flat_models: set[type[Union[BaseModel, Enum]]], + model_name_map: dict[type[Union[BaseModel, Enum]], str], +) -> dict[str, Any]: + definitions: dict[str, dict[str, Any]] = {} + + for model in flat_models: + if isinstance(model, type) and issubclass(model, BaseModel): + m_schema = model.model_json_schema(ref_template=REF_PREFIX + "{model}") + else: + m_schema = TypeAdapter(model).json_schema( + ref_template=REF_PREFIX + "{model}" + ) + + nested_defs = m_schema.pop("$defs", {}) + definitions.update(nested_defs) + + model_name = model_name_map[model] + + # Trim FastAPI-style docstrings if present + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] + + # Register schema under the resolved name + definitions[model_name] = m_schema + + return definitions + + +def get_rpc_flat_models_from_routes( + router: RPCRouter, +) -> Set[Union[Type[BaseModel], Type[Enum]]]: + body_fields_from_routes: List[ModelField] = [] + responses_from_routes: List[ModelField] = [] + + for rpc_method in router.rpc_methods.values(): + if not rpc_method.include_in_schema: + continue + + if rpc_method.arguments_field: + body_fields_from_routes.append( + rpc_method.arguments_field, + ) + if rpc_method.response_field: + responses_from_routes.append(rpc_method.response_field) + + if rpc_method.errors_models: + responses_from_routes.extend( + rpc_method.errors_models.values(), + ) + + return get_flat_models_from_fields( + body_fields_from_routes + responses_from_routes, + known_models=set(), + ) + + +def get_rpc_openapi_path( + *, + method_name: str, + route: RPCMethod, + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + security_scheme: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """Taken from FastAPI.""" + path = {} + + operation = get_openapi_rpc_metadata(name=method_name, route=route) + + request_body_oai = get_openapi_operation_rpc_args( + body_field=route.arguments_field, + model_name_map=model_name_map, + ) + if request_body_oai: + operation["requestBody"] = request_body_oai + + # - Successful response - + response_schema = get_schema_or_ref( + route.response_field, model_name_map, REF_PREFIX + ) + response_schema["title"] = route.response_field.name.replace("_", " ").title() + + operation.setdefault("responses", {}).setdefault(200, {}).update( + { + "description": "Successful response. **result** field:", + "content": {"application/json": {"schema": response_schema}}, + } + ) + + # - Errors - + if route.errors: + operation_errors = operation.setdefault("responses", {}) + for error_status_code, error_response in route.errors.items(): + process_response: Dict[str, Any] = {} + openapi_response = operation_errors.setdefault(str(error_status_code), {}) + + if route.errors_models and ( + field := route.errors_models[error_status_code] + ): + error_schema = get_schema_or_ref(field, model_name_map, REF_PREFIX) + + process_response.setdefault("content", {}).setdefault( + "application/json", {} + )["schema"] = error_schema + + description = error_response["description"] or "Error" + deep_dict_update(openapi_response, process_response) + openapi_response["description"] = f"**Error**: {description}" + + if security_scheme: + operation.setdefault("security", []).append(security_scheme) + + path["post"] = operation + return path + + +def get_openapi_operation_rpc_args( + *, + body_field: Optional[ModelField], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], +) -> Optional[Dict[str, Any]]: + if not body_field: + return None + + body_schema = get_schema_or_ref(body_field, model_name_map, REF_PREFIX) + + request_media_type = "application/json" + request_body_oai: Dict[str, Any] = {} + if body_field.required: + request_body_oai["required"] = True + + request_body_oai["content"] = {request_media_type: {"schema": body_schema}} + + return request_body_oai + + +def get_openapi_rpc_metadata(*, name: str, route: RPCMethod) -> Dict[str, Any]: + operation: Dict[str, Any] = { + "summary": route.handler.__name__.replace(".", " ").replace("_", " ").title(), + "description": route.handler.__doc__, + "operationId": ( + f"rpc_{name.replace('.', '_').replace(':', '_').replace('-', '_').lower()}" + ), + } + + if route.tags: + operation["tags"] = route.tags + + return operation diff --git a/pybotx_smartapp_rpc/openapi/utils.py b/pybotx_smartapp_rpc/openapi/utils.py new file mode 100644 index 0000000..1d78f04 --- /dev/null +++ b/pybotx_smartapp_rpc/openapi/utils.py @@ -0,0 +1,124 @@ +"""This module contains utility functions for OpenAPI generation.""" + +import re +from enum import Enum +from typing import Any, Dict, Sequence, Set, Type, Union + +from pydantic import BaseModel, TypeAdapter +from pydantic.fields import FieldInfo + +from pybotx_smartapp_rpc.models.model_field import ModelField + +TypeModelOrEnum = Union[Type[BaseModel], Type[Enum]] +TypeModelSet = Set[TypeModelOrEnum] + + +def get_flat_models_from_model( + model: Type[BaseModel], + known_models: TypeModelSet, +) -> TypeModelSet: + """ + Recursively collect BaseModel and Enum types from a model definition. + """ + if model in known_models: + return set() + + known_models.add(model) + flat_models: TypeModelSet = {model} + + for field in model.model_fields.values(): + flat_models |= get_flat_models_from_field(field, known_models) + + return flat_models + + +def get_flat_models_from_fields( + fields: Sequence[Union[ModelField, FieldInfo]], + known_models: TypeModelSet, +) -> TypeModelSet: + flat_models: TypeModelSet = set() + for field in fields: + flat_models |= get_flat_models_from_field(field, known_models) + return flat_models + + +def get_flat_models_from_field( + field: Union[ModelField, FieldInfo], + known_models: TypeModelSet, +) -> TypeModelSet: + flat_models: TypeModelSet = set() + field_type = _get_field_type(field) + + if isinstance(field_type, type): + if issubclass(field_type, BaseModel): + flat_models |= get_flat_models_from_model(field_type, known_models) + elif issubclass(field_type, Enum): + flat_models.add(field_type) + + return flat_models + + +def normalize_name(name: str) -> str: + return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name) + + +def get_long_model_name(model: TypeModelOrEnum) -> str: + return f"{model.__module__}__{model.__qualname__}".replace(".", "__") + + +def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]: + """ + Generate unique schema names, avoiding collisions by using module path. + """ + name_model_map: dict[str, TypeModelOrEnum] = {} + conflicting_names: Set[str] = set() + + for model in unique_models: + model_name = normalize_name(model.__name__) + if model_name in conflicting_names: + name_model_map[get_long_model_name(model)] = model + elif model_name in name_model_map: + conflicting_names.add(model_name) + other = name_model_map.pop(model_name) + name_model_map[get_long_model_name(other)] = other + name_model_map[get_long_model_name(model)] = model + else: + name_model_map[model_name] = model + + return {v: k for k, v in name_model_map.items()} + + +def deep_dict_update( + destination_dict: Dict[Any, Any], + source_dict: Dict[Any, Any], +) -> None: + for key in source_dict.keys(): + if ( + key in destination_dict + and isinstance(destination_dict[key], dict) + and isinstance(source_dict[key], dict) + ): + deep_dict_update(destination_dict[key], source_dict[key]) + else: + destination_dict[key] = source_dict[key] + + +def get_schema_or_ref( + model: ModelField, + model_name_map: dict[type[Union[BaseModel, Enum]], str], + ref_prefix: str, +) -> dict: + if model_name := model_name_map.get(model.type_): + return {"$ref": ref_prefix + model_name} + + return TypeAdapter(model.type_).json_schema(ref_template=ref_prefix + "{model}") + + +def _get_field_type(field: Union[ModelField, FieldInfo]) -> Any: + """Extract declared type from our wrapper or a native FieldInfo.""" + if isinstance(field, ModelField): + return field.type_ + elif isinstance(field, FieldInfo): + return field.annotation + else: + raise TypeError(f"Unsupported field type: {type(field)}") diff --git a/pybotx_smartapp_rpc/openapi_utils.py b/pybotx_smartapp_rpc/openapi_utils.py deleted file mode 100644 index 6abe73c..0000000 --- a/pybotx_smartapp_rpc/openapi_utils.py +++ /dev/null @@ -1,183 +0,0 @@ -from enum import Enum -from typing import Any, Dict, List, Optional, Set, Type, Union - -from pydantic import BaseModel -from pydantic.fields import ModelField -from pydantic.schema import ( - field_schema, - get_flat_models_from_fields, - model_process_schema, -) - -from pybotx_smartapp_rpc import RPCRouter -from pybotx_smartapp_rpc.models.method import RPCMethod - -REF_PREFIX = "#/components/schemas/" - - -def deep_dict_update( - destination_dict: Dict[Any, Any], - source_dict: Dict[Any, Any], -) -> None: - for key in source_dict.keys(): - if ( - key in destination_dict - and isinstance(destination_dict[key], dict) - and isinstance(source_dict[key], dict) - ): - deep_dict_update(destination_dict[key], source_dict[key]) - else: - destination_dict[key] = source_dict[key] - - -def get_rpc_flat_models_from_routes( - router: RPCRouter, -) -> Set[Union[Type[BaseModel], Type[Enum]]]: - body_fields_from_routes: List[ModelField] = [] - responses_from_routes: List[ModelField] = [] - - for method_name in router.rpc_methods.keys(): - if not router.rpc_methods[method_name].include_in_schema: - continue - - if router.rpc_methods[method_name].arguments_field: - body_fields_from_routes.append( - router.rpc_methods[method_name].arguments_field, # type: ignore - ) - if router.rpc_methods[method_name].response_field: - responses_from_routes.append(router.rpc_methods[method_name].response_field) - - if router.rpc_methods[method_name].errors_models: - responses_from_routes.extend( - router.rpc_methods[method_name].errors_models.values(), - ) - - return get_flat_models_from_fields( - body_fields_from_routes + responses_from_routes, - known_models=set(), - ) - - -def get_rpc_model_definitions( - *, - flat_models: Set[Union[Type[BaseModel], Type[Enum]]], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], -) -> Dict[str, Any]: - definitions: Dict[str, Dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, - model_name_map=model_name_map, - ref_prefix=REF_PREFIX, - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - definitions[model_name] = m_schema - return definitions - - -def get_openapi_operation_rpc_args( - *, - body_field: Optional[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], -) -> Optional[Dict[str, Any]]: - if not body_field: - return None - - body_schema, _, _ = field_schema( - body_field, - model_name_map=model_name_map, - ref_prefix=REF_PREFIX, - ) - request_media_type = "application/json" - required = body_field.required - request_body_oai: Dict[str, Any] = {} - if required: - request_body_oai["required"] = required - - request_media_content: Dict[str, Any] = {"schema": body_schema} - request_body_oai["content"] = {request_media_type: request_media_content} - return request_body_oai - - -def get_openapi_rpc_metadata(*, name: str, route: RPCMethod) -> Dict[str, Any]: - operation: Dict[str, Any] = { - "summary": route.handler.__name__.replace(".", " ").replace("_", " ").title(), - "description": route.handler.__doc__, - "operationId": ( - f"rpc_{name.replace('.', '_').replace(':', '_').replace('-', '_').lower()}" - ), - } - - if route.tags: - operation["tags"] = route.tags - - return operation - - -def get_rpc_openapi_path( # noqa: WPS231 - *, - method_name: str, - route: RPCMethod, - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], - security_scheme: Optional[Dict[str, Any]] = None, -) -> Dict[str, Any]: - """Taken from FastAPI.""" - path = {} - - operation = get_openapi_rpc_metadata(name=method_name, route=route) - - request_body_oai = get_openapi_operation_rpc_args( - body_field=route.arguments_field, - model_name_map=model_name_map, - ) - if request_body_oai: - operation["requestBody"] = request_body_oai - - # - Successful response - - response_schema, _, _ = field_schema( - route.response_field, - model_name_map=model_name_map, - ref_prefix=REF_PREFIX, - ) - - operation.setdefault("responses", {}).setdefault("ok", {}).update( - { - "description": "Successful response. **result** field:", - "content": {"application/json": {"schema": response_schema}}, - } - ) - - # - Errors - - if route.errors: - operation_errors = operation.setdefault("responses", {}) - for (error_status_code, error_response) in route.errors.items(): - process_response: Dict[str, Any] = {} - openapi_response = operation_errors.setdefault(str(error_status_code), {}) - - if route.errors_models and ( - field := route.errors_models[error_status_code] # noqa: WPS332 - ): - error_field_schema, _, _ = field_schema( - field, - model_name_map=model_name_map, - ref_prefix=REF_PREFIX, - ) - error_schema = ( - process_response.setdefault("content", {}) - .setdefault("application/json", {}) - .setdefault("schema", {}) - ) - deep_dict_update(error_schema, error_field_schema) - - description = error_response["description"] or "Error" - deep_dict_update(openapi_response, process_response) - openapi_response["description"] = f"**Error**: {description}" - - if security_scheme: - operation.setdefault("security", []).append(security_scheme) - - path["post"] = operation - return path diff --git a/pybotx_smartapp_rpc/router.py b/pybotx_smartapp_rpc/router.py index 8d245d1..31783b5 100644 --- a/pybotx_smartapp_rpc/router.py +++ b/pybotx_smartapp_rpc/router.py @@ -1,15 +1,16 @@ import inspect from enum import Enum -from typing import Callable, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union -from pydantic import BaseConfig -from pydantic.error_wrappers import ValidationError -from pydantic.fields import ModelField +from pydantic import BaseModel, ValidationError +from pydantic.fields import FieldInfo +from pydantic_core import PydanticUndefined from pybotx_smartapp_rpc import RPCError from pybotx_smartapp_rpc.empty_args import EmptyArgs from pybotx_smartapp_rpc.middlewares.empty_args_middleware import empty_args_middleware from pybotx_smartapp_rpc.models.method import RPCMethod +from pybotx_smartapp_rpc.models.model_field import ModelField from pybotx_smartapp_rpc.models.request import RPCRequest from pybotx_smartapp_rpc.models.responses import ( ResultType, @@ -21,6 +22,27 @@ class RPCRouter: + """ + Manages the registration, execution, and handling of Remote Procedure Calls (RPCs). + + This class is designed to facilitate the creation of RPC methods with support for + middlewares, error handling, and schema inclusion. Developers can utilize this class + to register RPC methods, include other routers, and handle incoming RPC requests. + + :ivar rpc_methods: Dictionary containing registered RPC method names as keys and + theircorresponding RPCMethod objects as values. + This maintains the set of methodsavailable for invocation. + :ivar middlewares: List of middleware instances to be applied to all RPC methods by + default. + :ivar tags: List of tags associated with the router, used for categorization or + documentation purposes. + :ivar include_in_schema: Boolean flag indicating whether the router and its methods + should be included in the autogenerated schema. + :ivar errors: List of error types specific to the router that can be associated with + its RPC methods. + These errors will be included in the schema or used for validation. + """ + def __init__( self, middlewares: Optional[List[Middleware]] = None, @@ -43,6 +65,27 @@ def method( errors: Optional[List[Type[RPCError]]] = None, include_in_schema: bool = True, ) -> Callable[[Handler], Handler]: + """ + Decorator, used to register the RPC method with appropriate configurations. + + Wrapped function should have attributes: + - smartapp: SmartApp + - argument: BaseModel + - any other arguments + + :param rpc_method_name: Unique name for the RPC method to be registered. + :param middlewares: List of middlewares objects. + If not provided, the default middlewares of the router are used. + :param return_type: The expected return type of the RPC method. + If not provided, it tries to infer the type. + :param tags: List of tags associated with the RPC method. + :param errors: List of possible error types the RPC method can raise. + If not provided, router-level errors are used. + :param include_in_schema: Boolean flag indicating whether this RPC method should + be included in the schema. + :return: A decorator function which can be applied to a handler to register it + as the specified RPC method. + """ if rpc_method_name in self.rpc_methods: raise ValueError(f"RPC method {rpc_method_name} already registered!") @@ -115,38 +158,68 @@ def include_router(self, router: "RPCRouter") -> None: rpc_method.errors_models = {**errors_models, **rpc_method.errors_models} self.rpc_methods[rpc_method_name] = rpc_method - def _get_args_and_return_field( - self, - handler: Handler, - return_type: Optional[Type[ResultType]] = None, - ) -> Tuple[Optional[ModelField], ModelField]: - signature = inspect.signature(handler) - return_annotation = signature.return_annotation - if hasattr(return_annotation, "__args__"): # noqa: WPS421 - response_type = return_annotation.__args__[0] - else: - response_type = None + @staticmethod + def _create_model_field( + name: str, + type_: Any, + default: Optional[Any] = PydanticUndefined, + field_info: Optional[FieldInfo] = None, + alias: Optional[str] = None, + ) -> ModelField: + field_info = field_info or FieldInfo( + annotation=type_, default=default, alias=alias + ) + return ModelField(name=name, field_info=field_info) - if return_type: - response_type = return_type + def _get_handler_request_argument_model( + self, handler_signature: inspect.Signature + ) -> Optional[ModelField]: + args_annotations = [ + arg[1].annotation for arg in handler_signature.parameters.items() + ] - response_field = ModelField( - name=f"Response_{handler.__name__}", - type_=response_type, - model_config=BaseConfig, - class_validators={}, + is_second_arg_pydantic_model = len(args_annotations) >= 2 and issubclass( + args_annotations[1], BaseModel ) - args_annotations = [arg[1].annotation for arg in signature.parameters.items()] - if len(args_annotations) >= 2: - arg_field = ModelField( + if is_second_arg_pydantic_model: + return self._create_model_field( name=str(args_annotations[1].__name__), type_=args_annotations[1], - model_config=BaseConfig, - class_validators={}, ) + return None + + def _get_handler_response_model( + self, + handler_signature: inspect.Signature, + return_type: Optional[Type[ResultType]], + name: str, + ) -> ModelField: + if return_type: + response_type = return_type else: - arg_field = None # type: ignore + return_annotation = handler_signature.return_annotation + if hasattr(return_annotation, "__args__"): + response_type = return_annotation.__args__[0] + else: + response_type = None + + return self._create_model_field( + name=name, + type_=response_type, + ) + + def _get_args_and_return_field( + self, + handler: Handler, + return_type: Optional[Type[ResultType]] = None, + ) -> tuple[Optional[ModelField], ModelField]: + signature = inspect.signature(handler) + + response_field = self._get_handler_response_model( + signature, return_type, f"Response_{handler.__name__}" + ) + arg_field = self._get_handler_request_argument_model(signature) return arg_field, response_field @@ -160,21 +233,18 @@ def _get_error_fields_and_models( return errors_fields, errors_models errors_fields = { - error.__fields__["id"].default: { - "description": error.__doc__ or error.__fields__["reason"].default, + error.model_fields["id"].default: { + "description": error.__doc__ or error.model_fields["reason"].default, } for error in errors - if error.__fields__["id"].default + if error.model_fields["id"].default } errors_models = { - error.__fields__["id"].default: ModelField( - name=error.__name__, - type_=error, - class_validators=None, - model_config=BaseConfig, + error.model_fields["id"].default: self._create_model_field( + name=error.__name__, type_=error ) for error in errors - if error.__fields__["id"].default + if error.model_fields["id"].default } return errors_fields, errors_models diff --git a/pybotx_smartapp_rpc/rpc.py b/pybotx_smartapp_rpc/rpc.py index 01e31ff..05a0933 100644 --- a/pybotx_smartapp_rpc/rpc.py +++ b/pybotx_smartapp_rpc/rpc.py @@ -26,7 +26,23 @@ class SmartAppRPC: - def __init__( # noqa: WPS234 + """ + Class for managing SmartApp RPC events and handling RPC requests with configured + routers, middlewares, and exception handlers. + + This class is responsible for processing incoming SmartApp events, generating + appropriate RPC responses, and managing the lifecycle of the associated RPC + request and response objects. It integrates middlewares, exception handling, + and a routing mechanism to facilitate proper processing of SmartApp events. + The class supports both asynchronous and synchronous event handling for improved + flexibility and integration with external systems. + + :ivar router: The main router that manages registered RPC routes and includes + middlewares and error handlers for handling RPC requests. + :type router: RPCRouter + """ + + def __init__( self, routers: List[RPCRouter], middlewares: Optional[List[Middleware]] = None, diff --git a/pybotx_smartapp_rpc/smartapp.py b/pybotx_smartapp_rpc/smartapp.py index 69d16a5..3f5f6a5 100644 --- a/pybotx_smartapp_rpc/smartapp.py +++ b/pybotx_smartapp_rpc/smartapp.py @@ -7,6 +7,25 @@ class SmartApp: + """ + Represents a SmartApp that integrates with a bot for communication and events. + + This class is designed to facilitate interaction with a bot to send smartapp + events and notifications. Its primary purpose is to provide managing + smartapp-related notifications and events within a chat context. + + :ivar bot: The bot instance used for communicating with the smart app. + :type bot: Bot + :ivar event: Optional event related to the smart app context. + :type event: Optional[SmartAppEvent] + :ivar bot_id: Unique identifier for the bot. + :type bot_id: UUID + :ivar chat_id: Unique identifier for the chat associated with the smart app. + :type chat_id: UUID + :ivar state: A simple namespace object for maintaining state information. + :type state: SimpleNamespace + """ + def __init__( self, bot: Bot, diff --git a/pyproject.toml b/pyproject.toml index cee30e4..39b3a4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,32 @@ [tool.poetry] name = "pybotx-smartapp-rpc" -version = "0.11.0" +version = "0.12.0" description = "eXpress SmartApp JSON-RPC library" authors = ["Arseniy Zhiltsov "] readme = "README.md" [tool.poetry.dependencies] -python = ">=3.8,<3.12" +python = ">=3.9,<3.14" -pydantic = ">=1.6.0,<1.11.0" -pybotx = ">=0.69.0" +pydantic = ">=2.8.2,<3.0" +pybotx = "~0.76.0a1" +deepdiff = "^8.6.0" -[tool.poetry.dev-dependencies] -add-trailing-comma = "^2.2.1" -black = "22.3.0" -isort = "5.10.1" -autoflake = "^1.4" -mypy = "0.910" -wemake-python-styleguide = "0.16.1" +fastapi = { version = "~0.115.1", optional = true } -pytest = "^7.0.1" -pytest-asyncio = "^0.18.1" -pytest-cov = "^4.1.0" +[tool.poetry.extras] +fastapi_utils = ["fastapi"] + +[tool.poetry.group.dev.dependencies] + +mypy = "1.16.1" +ruff = "0.12.0" + + +pytest = "^8.4.1" +pytest-asyncio = "^1.1.0" +pytest-cov = "^6.2.1" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core >=1.6.0"] build-backend = "poetry.core.masonry.api" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..39816b5 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,38 @@ +target-version = "py310" +line-length = 88 +exclude = ["app/db/migrations"] # папки/файлы, которые Ruff вообще не читает + +[lint] +# группы правил +select = ["E", "F", "B", "S", "W", "I", "N", "Q"] +extend-select = ["B006", "B007"] + +ignore = [ + # Bugbear + "B008", + # pydocstyle + "D101", "D102", "D103", "D106", "D107", "D202", + # whitespace-совместимость c Black + "E203", + # Bandit + "S101", "S702", +] + +[lint.per-file-ignores] +"*/__init__.py" = ["D104"] +"app/bot/commands/*.py" = ["D104"] +"app/resources/strings.py" = ["E501"] +"tests/*" = ["D100"] +# alembic migrations +"*/migrations/*" = ["ALL"] +"*/env.py" = ["ALL"] +# ignore test functons missing return annotations +"*/test_*.py" = ["ANN201", "ANN202"] + +[lint.flake8-quotes] +inline-quotes = "double" +avoid-escape = true + +[lint.isort] +known-first-party = ["app"] +combine-as-imports = true # from x import y as z, w as q \ No newline at end of file diff --git a/scripts/format b/scripts/format index 6625ae6..5a5fc27 100755 --- a/scripts/format +++ b/scripts/format @@ -1,10 +1,17 @@ #!/usr/bin/env bash +set -euo pipefail -set -ex - -autoflake --recursive --in-place \ - --remove-all-unused-imports \ - --ignore-init-module-imports \ +# ------------------------------------------------------------ +# 1. Удаляем неиспользуемые импорты (F401/F841) и +# сортируем оставшиеся (I###) — «исправляем» только эти коды. +# ------------------------------------------------------------ +ruff check \ + --select F401,F841,I \ + --fix \ pybotx_smartapp_rpc tests -isort --profile black pybotx_smartapp_rpc tests -black pybotx_smartapp_rpc tests + +# ------------------------------------------------------------ +# 2. Применяем Black-совместимое форматирование +# (эквивалент: `black app tests`) +# ------------------------------------------------------------ +ruff format pybotx_smartapp_rpc tests \ No newline at end of file diff --git a/scripts/lint b/scripts/lint index c4d170e..623c9df 100755 --- a/scripts/lint +++ b/scripts/lint @@ -1,9 +1,20 @@ #!/usr/bin/env bash +set -euo pipefail -set -ex +# --------------------------------------------------------------- +# 1. Проверяем форматирование (equivalent to: black --check --diff) +# --diff → вывод изменений +# --check → не править файлы, вернуть ненулевой код, если найдены проблемы +# --------------------------------------------------------------- +ruff format --check --diff pybotx_smartapp_rpc tests -black --check --diff pybotx_smartapp_rpc tests -isort --profile black --check-only pybotx_smartapp_rpc tests +# --------------------------------------------------------------- +# 2. Запускаем все lint-правила (B, E, F, S, B*, W*, N*, Q*, ...) +# Конфигурация берётся из pyproject.toml / ruff.toml +# --------------------------------------------------------------- +ruff check pybotx_smartapp_rpc tests -mypy pybotx_smartapp_rpc tests -flake8 pybotx_smartapp_rpc tests +# --------------------------------------------------------------- +# 3. Type-checking +# --------------------------------------------------------------- +mypy --config-file=setup.cfg pybotx_smartapp_rpc tests diff --git a/scripts/test b/scripts/test index 743b0a6..3093c7f 100755 --- a/scripts/test +++ b/scripts/test @@ -1,5 +1,88 @@ #!/usr/bin/env bash +# +# scripts/test – удобный раннер для pytest. +# +# ┌───────────────────────────────┐ +# │ ПРИМЕРЫ │ +# ├───────────────────────────────┤ +# │ ./scripts/test │ → все тесты │ +# │ ./scripts/test --unit -q │ → только unit, тихо │ +# │ ./scripts/test --integ -k api │ → только integ, по кею │ +# │ ./scripts/test --unit --integ │ → unit + integ │ +# └───────────────────────────────┘ +# +# Если скрипт запустили не через bash (например, `sh scripts/test`), +# он перезапустит себя под bash, чтобы работали массивы. +# --------------------------------------------------------------------- -set -ex +# --- re-exec в bash, если нужно -------------------------------------- +if [ -z "${BASH_VERSION:-}" ]; then + exec bash "$0" "$@" +fi -pytest ${@} +set -euo pipefail + +show_help() { + cat < Image: _file_mimetype="image/png", _file_hash="Jd9r+OKpw5y+FSCg1xNTSUkwEo4nCW1Sn1AkotkOpH0=", ) + + +@pytest.fixture +def request_factory() -> Callable[[dict[str, str]], Request]: + def _make_request(headers: Union[dict[str, str], None] = None) -> Request: + headers = headers or {} + # Starlette expects lowercase header names and bytes + raw_headers = [ + (k.lower().encode("utf-8"), v.encode("utf-8")) for k, v in headers.items() + ] + scope = { + "type": "http", + "http_version": "1.1", + "method": "GET", + "path": "/", + "query_string": b"", + "headers": raw_headers, + } + return Request(scope) + + return _make_request diff --git a/tests/test_openapi.py b/tests/test_openapi.py index f973717..615497d 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1,5 +1,9 @@ +from enum import Enum +from typing import Any + +import pytest +from deepdiff import DeepDiff from pydantic import BaseModel -from pydantic.schema import get_model_name_map from pybotx_smartapp_rpc import ( RPCError, @@ -8,11 +12,15 @@ SmartApp, SmartAppRPC, ) -from pybotx_smartapp_rpc.openapi_utils import ( - deep_dict_update, +from pybotx_smartapp_rpc.openapi.openapi import ( get_rpc_flat_models_from_routes, get_rpc_model_definitions, get_rpc_openapi_path, + update_fastapi_paths_by_rpc_router, +) +from pybotx_smartapp_rpc.openapi.utils import ( + deep_dict_update, + get_model_name_map, ) @@ -28,21 +36,35 @@ class Meta(BaseModel): user_id: int -class UserNotFound(RPCError): +class Color(str, Enum): + RED = "RED" + GREEN = "GREEN" + + +class Status(Enum): + OK = 1 + FAIL = 2 + + +class Item(BaseModel): + color: Color + + +class UserNotFoundError(RPCError): """Error description.""" - id = "UserNotFound" - reason = "User not found in system" + id: str = "UserNotFoundError" + reason: str = "User not found in system" meta: Meta -class OneUserNotFound(UserNotFound): - id = "OneUserNotFound" +class OneUserNotFoundError(UserNotFoundError): + id: str = "OneUserNotFoundError" class InvalidCredentialsError(RPCError): - id = "InvalidCredentialsError" - reason = "Invalid credentials" + id: str = "InvalidCredentialsError" + reason: str = "Invalid credentials" def test__deep_dict_update() -> None: @@ -68,7 +90,7 @@ async def get_api_version(smartapp: SmartApp, args: Meta) -> RPCResultResponse[i flat_rpc_models = get_rpc_flat_models_from_routes(rpc) - assert flat_rpc_models == {Response, UserArgs} + assert flat_rpc_models == {Response, UserArgs} # type: ignore async def test_flat_models_with_return_type() -> None: @@ -80,7 +102,7 @@ async def hidden(smartapp: SmartApp, rpc_args: UserArgs) -> RPCResultResponse[Me flat_rpc_models = get_rpc_flat_models_from_routes(rpc) - assert flat_rpc_models == {Response, UserArgs} + assert flat_rpc_models == {Response, UserArgs} # type: ignore async def test_get_rpc_openapi_path__without_args() -> None: @@ -101,12 +123,12 @@ async def get_api_version(smartapp: SmartApp) -> RPCResultResponse[int]: security_scheme={"auth": []}, ) - assert path == { + expected_path: dict[str, Any] = { "post": { "description": None, "operationId": "rpc_get_api_version", "responses": { - "ok": { + 200: { "content": { "application/json": { "schema": { @@ -123,18 +145,22 @@ async def get_api_version(smartapp: SmartApp) -> RPCResultResponse[int]: } } + diff = DeepDiff(expected_path, path) + + assert not diff, diff + async def test_collect_rpc_method_exists__with_errors() -> None: # - Arrange - rpc = RPCRouter(tags=["rpc"], errors=[InvalidCredentialsError]) - @rpc.method("get_user", errors=[UserNotFound], tags=["user"]) + @rpc.method("get_user", errors=[UserNotFoundError], tags=["user"]) async def get_api_version( smartapp: SmartApp, rpc_args: UserArgs ) -> RPCResultResponse[int]: return RPCResultResponse(result=42) - smartapp_rpc = SmartAppRPC(routers=[rpc], errors=[OneUserNotFound]) + smartapp_rpc = SmartAppRPC(routers=[rpc], errors=[OneUserNotFoundError]) rpc_model_name_map = get_model_name_map( get_rpc_flat_models_from_routes(smartapp_rpc.router) ) @@ -146,7 +172,7 @@ async def get_api_version( model_name_map=rpc_model_name_map, ) - assert path == { + expected_path = { "post": { "summary": "Get Api Version", "tags": ["rpc", "user"], @@ -161,7 +187,7 @@ async def get_api_version( }, }, "responses": { - "ok": { + 200: { "description": "Successful response. **result** field:", "content": { "application/json": { @@ -172,18 +198,20 @@ async def get_api_version( } }, }, - "UserNotFound": { + "UserNotFoundError": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/UserNotFound"} + "schema": {"$ref": "#/components/schemas/UserNotFoundError"} } }, "description": "**Error**: Error description.", }, - "OneUserNotFound": { + "OneUserNotFoundError": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/OneUserNotFound"} + "schema": { + "$ref": "#/components/schemas/OneUserNotFoundError" + } } }, "description": "**Error**: User not found in system", @@ -202,12 +230,15 @@ async def get_api_version( } } + diff = DeepDiff(expected_path, path, ignore_order=True) + assert not diff, diff + async def test_get_rpc_model_definition() -> None: # - Arrange - rpc = RPCRouter(tags=["rpc"]) - @rpc.method("get_user", errors=[UserNotFound], tags=["user"]) + @rpc.method("get_user", errors=[UserNotFoundError], tags=["user"]) async def get_api_version( smartapp: SmartApp, rpc_args: UserArgs ) -> RPCResultResponse[int]: @@ -223,7 +254,7 @@ async def get_api_version( flat_models=flat_rpc_models, model_name_map=rpc_model_name_map ) - assert rpc_definitions == { + expected_definitions = { "Meta": { "properties": {"user_id": {"title": "User Id", "type": "integer"}}, "required": ["user_id"], @@ -236,19 +267,174 @@ async def get_api_version( "title": "UserArgs", "type": "object", }, - "UserNotFound": { + "UserNotFoundError": { "description": "Error description.", "properties": { - "id": {"default": "UserNotFound", "title": "Id", "type": "string"}, + "id": {"default": "UserNotFoundError", "title": "Id", "type": "string"}, "meta": {"$ref": "#/components/schemas/Meta"}, "reason": { - "default": "User not found in " "system", + "default": "User not found in system", "title": "Reason", "type": "string", }, }, "required": ["meta"], - "title": "UserNotFound", + "title": "UserNotFoundError", "type": "object", }, } + + diff = DeepDiff(expected_definitions, rpc_definitions, ignore_order=True) + assert not diff, diff + + +def test_update_fastapi_paths_adds_security_schemas_and_paths(): + # Arrange + rpc = RPCRouter(tags=["rpc"]) + + @rpc.method("get_user", errors=[UserNotFoundError], tags=["user"]) + async def get_user(smartapp: SmartApp, args: UserArgs) -> RPCResultResponse[int]: + return RPCResultResponse(result=1) + + # pre-existing OpenAPI dict with unrelated path to ensure non-destructive merge + openapi_dict = { + "openapi": "3.1.0", + "info": {"title": "Test API", "version": "1.0.0"}, + "paths": {"/health": {"get": {"responses": {"200": {"description": "ok"}}}}}, + "components": {"schemas": {"Existing": {"type": "object"}}}, + } + + security_definitions = { + "RPC Auth": { + "type": "apiKey", + "in": "header", + "name": "X-RPC-AUTH", + "description": "...", + } + } + operation_security = {"RPC Auth": []} + + # Act + update_fastapi_paths_by_rpc_router( + openapi_dict, + rpc_router=rpc, + security_definitions=security_definitions, + operation_security=operation_security, + ) + + # Assert: security schemes merged + assert "components" in openapi_dict + assert "securitySchemes" in openapi_dict["components"] + assert openapi_dict["components"]["securitySchemes"]["RPC Auth"]["type"] == "apiKey" + + # Assert: new RPC path exists and has POST operation with security + assert "/get_user" in openapi_dict["paths"] + post_op = openapi_dict["paths"]["/get_user"]["post"] + assert post_op["operationId"] == "rpc_get_user" + assert {"RPC Auth": []} in post_op.get("security", []) + + # Request body should be present (because of args) + assert post_op["requestBody"]["content"]["application/json"] + + # Responses include success and error codes by id + responses = post_op["responses"] + assert "200" in { + str(k) for k in responses.keys() + } # numeric key 200 serialized later + assert "UserNotFoundError" in responses + # Error references use $ref to components/schemas + err_schema = responses["UserNotFoundError"]["content"]["application/json"]["schema"] + assert err_schema == {"$ref": "#/components/schemas/UserNotFoundError"} + + # Assert: schemas got updated with our models, while preserving existing schema + schemas = openapi_dict["components"]["schemas"] + assert "Existing" in schemas # preserved + # UserArgs schema present + assert "UserArgs" in schemas + # UserNotFound schema present and description trimmed + assert "UserNotFoundError" in schemas + assert schemas["UserNotFoundError"]["title"] == "UserNotFoundError" + + +def test_update_fastapi_paths_skips_hidden_and_handles_no_definitions(): + # Arrange + rpc = RPCRouter() + + # method with primitive response, no args, included + @rpc.method("get_api_version") + async def get_api_version(smartapp: SmartApp) -> RPCResultResponse[int]: + return RPCResultResponse(result=1) + + # method hidden from schema + @rpc.method("__hidden_method", include_in_schema=False) + async def hidden(smartapp: SmartApp, args: UserArgs) -> RPCResultResponse[int]: + return RPCResultResponse(result=1) + + openapi_dict = {"openapi": "3.1.0", "info": {"title": "T", "version": "1"}} + + # Act + update_fastapi_paths_by_rpc_router(openapi_dict, rpc) + + # Assert: only visible method path is added + assert "/get_api_version" in openapi_dict["paths"] + assert "/__hidden_method" not in openapi_dict["paths"] + + # Because only primitive types are involved, there should be no schemas added + # The function adds schemas only if there are rpc_definitions + components = openapi_dict.get("components", {}) + assert "schemas" not in components or components.get("schemas") == {} + + +def test_update_fastapi_paths_merges_into_existing_empty_sections_gracefully(): + # Arrange: empty containers + rpc = RPCRouter() + + @rpc.method("ping") + async def ping(smartapp: SmartApp) -> RPCResultResponse[str]: + return RPCResultResponse(result="pong") + + # Existing dict without components/paths + openapi_dict = {"openapi": "3.1.0", "info": {"title": "X", "version": "1"}} + + # Act + update_fastapi_paths_by_rpc_router(openapi_dict, rpc) + + # Assert minimal structure created and path added + assert "paths" in openapi_dict + assert "/ping" in openapi_dict["paths"] + + +@pytest.mark.parametrize( + "enum_cls, expected_type, expected_values", + [ + (Color, "string", ["RED", "GREEN"]), + (Status, "integer", [1, 2]), + ], +) +def test_openapi_definitions_for_enums(enum_cls, expected_type, expected_values): + defs = get_rpc_model_definitions( + flat_models={enum_cls}, + model_name_map={enum_cls: enum_cls.__name__}, + ) + + enum_schema = defs[enum_cls.__name__] + assert enum_schema["enum"] == expected_values + assert enum_schema["type"] == expected_type + + +def test_openapi_definitions_for_model_with_enum_field(): + defs = get_rpc_model_definitions( + flat_models={Item, Color}, + model_name_map={Item: "Item", Color: "Color"}, + ) + + assert "Color" in defs + assert "Item" in defs + + item_schema = defs["Item"] + assert item_schema["type"] == "object" + props = item_schema["properties"] + assert "color" in props + # Убедимся, что поле color ссылается на определение Color + assert "$ref" in props["color"] + assert props["color"]["$ref"].endswith("/Color") diff --git a/tests/test_rpc_calls.py b/tests/test_rpc_calls.py index 316b60d..61a897c 100644 --- a/tests/test_rpc_calls.py +++ b/tests/test_rpc_calls.py @@ -2,6 +2,8 @@ from unittest.mock import AsyncMock from uuid import UUID +import pytest +from deepdiff import DeepDiff from pybotx import ( BotAPISyncSmartAppEventErrorResponse, BotAPISyncSmartAppEventResultResponse, @@ -109,13 +111,14 @@ async def sum_handler(smartapp: SmartApp, args: SumArgs) -> RPCResultResponse[in "status": "error", "errors": [ { - "reason": "value is not a valid integer", - "id": "TYPE_ERROR", + "reason": "Input should be a valid integer, " + "unable to parse string as an integer", + "id": "INT_PARSING", "meta": {"location": ("first",)}, }, { - "reason": "field required", - "id": "VALUE_ERROR", + "reason": "Field required", + "id": "MISSING", "meta": {"location": ("second",)}, }, ], @@ -204,8 +207,8 @@ async def get_api_version(smartapp: SmartApp) -> RPCResultResponse[int]: "status": "error", "errors": [ { - "reason": "Invalid RPC request: field required", - "id": "VALUE_ERROR", + "reason": "Invalid RPC request: Field required", + "id": "MISSING", "meta": {"field": "method"}, }, ], @@ -310,7 +313,7 @@ async def sum_handler( args: SumArgs, ) -> RPCResultResponse[SumResponse]: return RPCResultResponse( - SumResponse(call_result=args.first_arg + args.second_arg), + SumResponse(callResult=args.first_arg + args.second_arg), ) smartapp_rpc = SmartAppRPC(routers=[rpc]) @@ -392,10 +395,12 @@ async def get_api_version(smartapp: SmartApp) -> RPCResultResponse[int]: ) # - Assert - - assert response == BotAPISyncSmartAppEventResultResponse.from_domain( + expected_object = BotAPISyncSmartAppEventResultResponse.from_domain( data=1, files=Undefined, ) + diff = DeepDiff(response.model_dump(), expected_object.model_dump()) + assert not diff, diff async def test_handle_sync_smartapp_event_rpc_error_returned( @@ -425,7 +430,7 @@ async def get_api_version(smartapp: SmartApp) -> RPCErrorResponse: ) # - Assert - - assert response == BotAPISyncSmartAppEventErrorResponse.from_domain( + expected_response = BotAPISyncSmartAppEventErrorResponse.from_domain( errors=[ { "reason": "Api version undefined", @@ -435,6 +440,9 @@ async def get_api_version(smartapp: SmartApp) -> RPCErrorResponse: ] ) + diff = DeepDiff(response.model_dump(), expected_response.model_dump()) + assert not diff, diff + async def test_handle_sync_smartapp_event_with_wrong_args( smartapp_event_factory: Callable[..., SmartAppEvent], @@ -463,21 +471,25 @@ async def sum_handler(smartapp: SmartApp, args: SumArgs) -> RPCResultResponse[in ) # - Assert - - assert response == BotAPISyncSmartAppEventErrorResponse.from_domain( + expected_response = BotAPISyncSmartAppEventErrorResponse.from_domain( errors=[ { - "reason": "value is not a valid integer", - "id": "TYPE_ERROR", + "reason": "Input should be a valid integer, " + "unable to parse string as an integer", + "id": "INT_PARSING", "meta": {"location": ("first",)}, }, { - "reason": "field required", - "id": "VALUE_ERROR", + "reason": "Field required", + "id": "MISSING", "meta": {"location": ("second",)}, }, ] ) + diff = DeepDiff(response.model_dump(), expected_response.model_dump()) + assert not diff, diff + async def test_handle_sync_smartapp_event_wrong_rpc_request( smartapp_event_factory: Callable[..., SmartAppEvent], @@ -504,12 +516,57 @@ async def get_api_version(smartapp: SmartApp) -> RPCResultResponse[int]: ) # - Assert - - assert response == BotAPISyncSmartAppEventErrorResponse.from_domain( + expected_response = BotAPISyncSmartAppEventErrorResponse.from_domain( errors=[ { - "reason": "Invalid RPC request: field required", - "id": "VALUE_ERROR", + "reason": "Invalid RPC request: Field required", + "id": "MISSING", "meta": {"field": "method"}, }, ] ) + + diff = DeepDiff(response.model_dump(), expected_response.model_dump()) + assert not diff, diff + + +@pytest.mark.asyncio +async def test_rpc_call_with_middleware_effect(smartapp_event_factory, bot): + applied = [] + + # Middleware to track execution + + async def router_mw(smartapp: SmartApp, args, call_next): + applied.append("router_mw") + response = await call_next(smartapp, args) + return response + + async def method_mw(smartapp: SmartApp, args, call_next): + applied.append("method_mw") + response = await call_next(smartapp, args) + return response + + # Create router with our middleware + rpc = RPCRouter(middlewares=[router_mw]) + + @rpc.method("test", middlewares=[method_mw]) + async def test_method( + smartapp: SmartApp, + ): + applied.append("handler") + return RPCResultResponse(result=123) + + # Include router in SmartAppRPC + smartapp_rpc = SmartAppRPC(routers=[rpc]) + + # Create event with empty params + event = smartapp_event_factory( + "test", + ) + + # Execute the RPC call + result = await smartapp_rpc.handle_sync_smartapp_event(event, bot) + + # Assertions + assert applied == ["router_mw", "method_mw", "handler"] + assert result.data == 123 diff --git a/tests/test_rpc_router.py b/tests/test_rpc_router.py index 82c8901..786680a 100644 --- a/tests/test_rpc_router.py +++ b/tests/test_rpc_router.py @@ -1,6 +1,9 @@ +from unittest.mock import AsyncMock, MagicMock + import pytest from pybotx_smartapp_rpc import RPCResultResponse, RPCRouter, SmartApp +from pybotx_smartapp_rpc.middlewares.empty_args_middleware import empty_args_middleware async def test_collect_rpc_method_exists() -> None: @@ -43,3 +46,41 @@ async def other_get_api_version(smartapp: SmartApp) -> RPCResultResponse[int]: # - Assert - assert "get_api_version" in str(exc.value) assert "already registered" in str(exc.value) + + +def test_middlewares_order(): + """Check the order in which mixed middlewares are stored + Note - the last middleware is added automatically + """ + + router_middleware = AsyncMock() + method_middleware = AsyncMock() + + rpc = RPCRouter(middlewares=[router_middleware]) + + @rpc.method("test_method", middlewares=[method_middleware]) + async def test_method(smartapp): + pass + + rpc_method = rpc.rpc_methods["test_method"] + + # Check the order of middlewares + assert rpc_method.middlewares[0] is router_middleware + assert rpc_method.middlewares[1] is method_middleware + assert rpc_method.middlewares[-1] is empty_args_middleware + + +async def test_rpc_router_call_method_directly(): + rpc = RPCRouter() + + called = False + + @rpc.method("dummy") + async def dummy_method(smartapp: SmartApp): + nonlocal called + called = True + return RPCResultResponse(result=42) + + response = await rpc.rpc_methods["dummy"].handler(MagicMock()) + assert called + assert response.result == 42 diff --git a/tests/test_security.py b/tests/test_security.py new file mode 100644 index 0000000..414f72e --- /dev/null +++ b/tests/test_security.py @@ -0,0 +1,108 @@ +from uuid import UUID, uuid4 + +import pytest +from fastapi import HTTPException +from starlette.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN + +from pybotx_smartapp_rpc.fastapi_utils.security import RPCAuth + + +@pytest.mark.asyncio +async def test_rpcauth_with_full_header_parses_values(request_factory): + # Arrange + default_bot_id = uuid4() # shouldn't be used when provided in header + ra = RPCAuth(bot_id=default_bot_id) + + bot_id = uuid4() + sender_huid = uuid4() + sender_udid = uuid4() + chat_id = uuid4() + + header_value = ( + f"bot_id={bot_id}&sender_huid={sender_huid}" + f"&sender_udid={sender_udid}&chat_id={chat_id}" + ) + req = request_factory({"X-RPC-AUTH": header_value}) + + # Act + cfg = await ra(req) + + # Assert + assert cfg.bot_id == bot_id + assert cfg.sender_huid == sender_huid + assert cfg.sender_udid == sender_udid + assert cfg.chat_id == chat_id + + +@pytest.mark.asyncio +async def test_rpcauth_with_invalid_format_raises_403(request_factory): + # Arrange + ra = RPCAuth(bot_id=uuid4()) + # Invalid format: no key=value pairs + req = request_factory({"X-RPC-AUTH": "not-a-key-value-pair"}) + + # Act / Assert + with pytest.raises(HTTPException) as ei: + await ra(req) + + exc = ei.value + assert exc.status_code == HTTP_403_FORBIDDEN + assert exc.detail == "Invalid RPC Auth format" + + +@pytest.mark.asyncio +async def test_rpcauth_with_invalid_uuid_values_raises_403(request_factory): + # Arrange + ra = RPCAuth(bot_id=uuid4()) + + # bot_id invalid UUID + req = request_factory({"X-RPC-AUTH": "bot_id=not-a-uuid"}) + + # Act / Assert + with pytest.raises(HTTPException) as ei: + await ra(req) + + exc = ei.value + assert exc.status_code == HTTP_400_BAD_REQUEST + + +@pytest.mark.asyncio +async def test_rpcauth_header_without_bot_id_uses_default_from_constructor( + request_factory, +): + # This test documents the intended behavior that if the header is present but + # bot_id is not specified, + # the RPCAuth should fall back to the configured default bot_id. + + # Arrange + default_bot_id = uuid4() + ra = RPCAuth(bot_id=default_bot_id) + + sender_huid = uuid4() + header_value = f"sender_huid={sender_huid}" + req = request_factory({"X-RPC-AUTH": header_value}) + + # Act + cfg = await ra(req) + + # Assert + assert cfg.bot_id == default_bot_id # expected fallback + assert cfg.sender_huid == sender_huid + # Others should be auto-filled UUIDs + assert isinstance(cfg.sender_udid, UUID) + assert isinstance(cfg.chat_id, UUID) + + +@pytest.mark.asyncio +async def test_rpcauth_with_empty_bot_id_in_header_raises_400(request_factory): + ra = RPCAuth(bot_id=uuid4()) + # bot_id is present but empty -> should NOT fallback, should raise 400 + req = request_factory( + {"X-RPC-AUTH": "bot_id=&sender_huid=00000000-0000-0000-0000-000000000000"} + ) + + with pytest.raises(HTTPException) as ei: + await ra(req) + + exc = ei.value + assert exc.status_code == HTTP_400_BAD_REQUEST