From 5fb05527b97ddfbf0a9bcd0a3bdd94b8f51ea375 Mon Sep 17 00:00:00 2001 From: Aleksander Maksimeniuk <78569692+alexhook@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:14:55 +0300 Subject: [PATCH] feat: add handler for synchronous smartapp events (#471) --- README.md | 36 ++ poetry.lock | 368 +++++++++++------- pybotx/__init__.py | 14 +- pybotx/bot/bot.py | 50 +++ pybotx/bot/handler.py | 8 + pybotx/bot/handler_collector.py | 79 +++- pybotx/client/smartapps_api/exceptions.py | 5 + pybotx/client/smartapps_api/smartapp_event.py | 11 +- .../smartapps_api/sync_smartapp_event.py | 7 + pyproject.toml | 6 +- setup.cfg | 5 +- tests/conftest.py | 136 +++++++ tests/test_base_command.py | 118 ++++++ tests/test_end_to_end.py | 144 +++++++ tests/test_handler_collector.py | 55 ++- 15 files changed, 883 insertions(+), 159 deletions(-) create mode 100644 pybotx/client/smartapps_api/exceptions.py create mode 100644 pybotx/client/smartapps_api/sync_smartapp_event.py diff --git a/README.md b/README.md index 520310ff..db365270 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,16 @@ async def command_handler(request: Request) -> JSONResponse: ) +# На этот эндпоинт приходят события BotX для SmartApps, обрабатываемые синхронно. +@app.post("/smartapps/request") +async def sync_smartapp_event_handler(request: Request) -> JSONResponse: + response = await bot.sync_execute_raw_smartapp_event( + await request.json(), + request_headers=request.headers, + ) + return JSONResponse(response.jsonable_dict(), status_code=HTTPStatus.OK) + + # К этому эндпоинту BotX обращается, чтобы узнать # доступность бота и его список команд. @app.get("/status") @@ -212,6 +222,32 @@ async def smartapp_event_handler(event: SmartAppEvent, bot: Bot) -> None: ``` +### Получение синхронных SmartApp событий + +```python +from pybotx import * + +collector = HandlerCollector() + + +# Обработчик синхронных Smartapp событий, приходящих на эндпоинт `/smartapps/request` +@collector.sync_smartapp_event +async def handle_sync_smartapp_event( + event: SmartAppEvent, bot: Bot, +) -> SyncSmartAppEventResponsePayload: + print(f"Got sync smartapp event: {event}") + return SyncSmartAppEventResponsePayload.from_domain( + ref=event.ref, + smartapp_id=event.bot.id, + chat_id=event.chat.id, + data={}, + opts={}, + files=[], + encrypted=True, + ) +``` + + ### Middlewares *(Этот функционал относится исключительно к `pybotx`)* diff --git a/poetry.lock b/poetry.lock index fd74de86..ae99afc4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "add-trailing-comma" version = "2.2.1" description = "Automatically add trailing commas to calls and literals" +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -18,6 +19,7 @@ tokenize-rt = ">=3.0.1" name = "aiocsv" version = "1.2.5" description = "Asynchronous CSV reading/writing" +category = "main" optional = false python-versions = ">=3.6, <4" files = [ @@ -52,6 +54,7 @@ files = [ name = "aiofiles" version = "23.2.1" description = "File support for asyncio." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -61,13 +64,14 @@ files = [ [[package]] name = "anyio" -version = "4.3.0" +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.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] [package.dependencies] @@ -83,13 +87,14 @@ trio = ["trio (>=0.23)"] [[package]] name = "asgiref" -version = "3.7.2" +version = "3.8.1" description = "ASGI specs, helper code, and adapters" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [package.dependencies] @@ -102,6 +107,7 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 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 = [ @@ -113,6 +119,7 @@ files = [ name = "attrs" version = "23.2.0" description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -132,6 +139,7 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p name = "autoflake" version = "1.7.8" description = "Removes unused imports and unused variables" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -147,6 +155,7 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} name = "bandit" version = "1.7.2" description = "Security oriented static analyser for python code." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -169,6 +178,7 @@ yaml = ["PyYAML"] name = "black" version = "22.3.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -213,19 +223,21 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -325,6 +337,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -339,6 +352,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} 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 = [ @@ -348,63 +362,64 @@ files = [ [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.3" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, + {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, + {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, + {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, + {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, + {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, + {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, + {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, + {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, + {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, + {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, + {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, + {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, ] [package.dependencies] @@ -417,6 +432,7 @@ toml = ["tomli"] name = "darglint" version = "1.8.1" description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -428,17 +444,16 @@ files = [ name = "docutils" version = "0.20.1" description = "Docutils -- Python Documentation Utilities" +category = "dev" optional = false -python-versions = ">=3.7" -files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, -] +python-versions = "*" +files = [] [[package]] name = "eradicate" version = "2.3.0" description = "Removes commented-out code." +category = "dev" optional = false python-versions = "*" files = [ @@ -448,13 +463,14 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -464,6 +480,7 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.95.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -485,6 +502,7 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6 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 = [ @@ -501,6 +519,7 @@ pyflakes = ">=2.4.0,<2.5.0" name = "flake8-bandit" version = "2.1.2" description = "Automated security testing with bandit and flake8." +category = "dev" optional = false python-versions = "*" files = [ @@ -517,6 +536,7 @@ pycodestyle = "*" 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 = [ @@ -531,6 +551,7 @@ flake8 = ">=3.5,<5" name = "flake8-bugbear" version = "21.11.29" 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.6" files = [ @@ -549,6 +570,7 @@ dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] name = "flake8-commas" version = "2.1.0" description = "Flake8 lint for trailing commas." +category = "dev" optional = false python-versions = "*" files = [ @@ -563,6 +585,7 @@ flake8 = ">=2" name = "flake8-comprehensions" version = "3.14.0" description = "A flake8 plugin to help you write better list/set/dict comprehensions." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -577,6 +600,7 @@ flake8 = ">=3.0,<3.2.0 || >3.2.0" name = "flake8-debugger" version = "4.1.2" description = "ipdb/pdb statement checker plugin for flake8" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -592,6 +616,7 @@ pycodestyle = "*" 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 = [ @@ -607,6 +632,7 @@ pydocstyle = ">=2.1" 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 = [ @@ -623,6 +649,7 @@ flake8 = ">=3.5,<6" name = "flake8-isort" version = "4.2.0" description = "flake8 plugin that integrates isort ." +category = "dev" optional = false python-versions = "*" files = [ @@ -641,6 +668,7 @@ test = ["pytest-cov"] name = "flake8-polyfill" version = "1.0.2" description = "Polyfill package for Flake8 plugins" +category = "dev" optional = false python-versions = "*" files = [ @@ -655,6 +683,7 @@ flake8 = "*" name = "flake8-quotes" version = "3.4.0" description = "Flake8 lint for quotes." +category = "dev" optional = false python-versions = "*" files = [ @@ -669,6 +698,7 @@ setuptools = "*" name = "flake8-rst-docstrings" version = "0.2.7" description = "Python docstring reStructuredText (RST) validator" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -685,6 +715,7 @@ restructuredtext-lint = "*" name = "flake8-string-format" version = "0.3.0" description = "string format checker, plugin for flake8" +category = "dev" optional = false python-versions = "*" files = [ @@ -699,6 +730,7 @@ flake8 = "*" name = "gitdb" version = "4.0.11" description = "Git Object Database" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -711,25 +743,28 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.42" +version = "3.1.43" description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, - {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] +doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -741,6 +776,7 @@ files = [ name = "httpcore" version = "1.0.2" description = "A minimal low-level HTTP client." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -755,13 +791,14 @@ h11 = ">=0.13,<0.15" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] trio = ["trio (>=0.22.0,<0.23.0)"] [[package]] name = "httpx" version = "0.25.2" description = "The next generation HTTP client." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -772,31 +809,33 @@ files = [ [package.dependencies] anyio = "*" certifi = "*" -httpcore = "==1.*" +httpcore = ">=1.0.0,<2.0.0" idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -808,6 +847,7 @@ files = [ 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" files = [ @@ -825,6 +865,7 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "loguru" version = "0.6.0" description = "Python logging made (stupidly) simple" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -843,6 +884,7 @@ dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = "*" files = [ @@ -854,6 +896,7 @@ files = [ name = "mypy" version = "0.910" description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -895,6 +938,7 @@ python2 = ["typed-ast (>=1.4.0,<1.5.0)"] name = "mypy-extensions" version = "0.4.4" description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" optional = false python-versions = ">=2.7" files = [ @@ -903,19 +947,21 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[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 = [ @@ -927,6 +973,7 @@ files = [ name = "pbr" version = "6.0.0" description = "Python Build Reasonableness" +category = "dev" optional = false python-versions = ">=2.6" files = [ @@ -938,6 +985,7 @@ files = [ name = "pep8-naming" version = "0.12.1" description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" optional = false python-versions = "*" files = [ @@ -951,28 +999,31 @@ flake8-polyfill = ">=1.0.2,<2" [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +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.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {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.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -983,6 +1034,7 @@ testing = ["pytest", "pytest-benchmark"] 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 = [ @@ -992,47 +1044,48 @@ files = [ [[package]] name = "pydantic" -version = "1.10.14" +version = "1.10.15" description = "Data validation and settings management using python type hints" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, - {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, - {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, - {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, - {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, - {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, - {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, - {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, - {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, - {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, + {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"}, + {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"}, + {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"}, + {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"}, + {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"}, + {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"}, + {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"}, + {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"}, + {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"}, + {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"}, + {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"}, + {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"}, + {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"}, + {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"}, + {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"}, + {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"}, + {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"}, + {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"}, + {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"}, + {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"}, + {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"}, + {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"}, + {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"}, + {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"}, + {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"}, + {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"}, + {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"}, + {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"}, + {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"}, + {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"}, + {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"}, + {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"}, + {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"}, + {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"}, + {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"}, + {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"}, ] [package.dependencies] @@ -1046,6 +1099,7 @@ email = ["email-validator (>=1.0.3)"] name = "pydocstyle" version = "6.3.0" description = "Python docstring style checker" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1063,6 +1117,7 @@ toml = ["tomli (>=1.2.3)"] 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 = [ @@ -1072,23 +1127,24 @@ files = [ [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" version = "2.8.0" description = "JSON Web Token implementation in Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1106,6 +1162,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pytest" version = "7.2.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1129,6 +1186,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-asyncio" version = "0.16.0" description = "Pytest support for asyncio." +category = "dev" optional = false python-versions = ">= 3.6" files = [ @@ -1146,6 +1204,7 @@ testing = ["coverage", "hypothesis (>=5.7.1)"] name = "pytest-cov" version = "4.0.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1164,6 +1223,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1223,6 +1283,7 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1244,6 +1305,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "respx" version = "0.20.2" description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1258,6 +1320,7 @@ httpx = ">=0.21.0" name = "restructuredtext-lint" version = "1.4.0" description = "reStructuredText linter" +category = "dev" optional = false python-versions = "*" files = [ @@ -1269,24 +1332,25 @@ docutils = ">=0.11,<1.0" [[package]] name = "setuptools" -version = "69.2.0" +version = "70.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, + {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["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"] +testing = ["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)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "smmap" version = "5.0.1" description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1298,6 +1362,7 @@ files = [ 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 = [ @@ -1309,6 +1374,7 @@ files = [ 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 = "*" files = [ @@ -1320,6 +1386,7 @@ files = [ name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1338,6 +1405,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "stevedore" version = "5.2.0" description = "Manage dynamic plugins for Python applications" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1352,6 +1420,7 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" 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 = [ @@ -1363,6 +1432,7 @@ files = [ 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 = [ @@ -1374,6 +1444,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1383,19 +1454,21 @@ files = [ [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1413,6 +1486,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "uvicorn" version = "0.16.0" description = "The lightning-fast ASGI server." +category = "dev" optional = false python-versions = "*" files = [ @@ -1432,6 +1506,7 @@ standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (>=0.2.0,<0.4.0)", name = "wemake-python-styleguide" version = "0.16.0" description = "The strictest and most opinionated python linter ever" +category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -1464,6 +1539,7 @@ typing_extensions = ">=3.6,<5.0" name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1477,4 +1553,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.13" -content-hash = "0fceb89488ee45d0819b2b61dd360f5740b68d7de3d109ea2c09040621e077fb" +content-hash = "92c36a662fcdb927944e42e8272d20124d8de76aafa345caa1527735c51a2229" diff --git a/pybotx/__init__.py b/pybotx/__init__.py index 7a8d3c32..1c20c086 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -23,7 +23,11 @@ UnknownBotAccountError, UnverifiedRequestError, ) -from pybotx.bot.handler import IncomingMessageHandlerFunc, Middleware +from pybotx.bot.handler import ( + IncomingMessageHandlerFunc, + Middleware, + SyncSmartAppEventHandlerFunc, +) from pybotx.bot.handler_collector import HandlerCollector from pybotx.bot.testing import lifespan_wrapper from pybotx.client.exceptions.callbacks import ( @@ -55,10 +59,14 @@ StealthModeDisabledError, ) from pybotx.client.exceptions.users import UserNotFoundError +from pybotx.client.smartapps_api.exceptions import SyncSmartAppEventHandlerNotFoundError from pybotx.client.smartapps_api.smartapp_manifest import ( SmartappManifest, SmartappManifestWebParams, ) +from pybotx.client.smartapps_api.sync_smartapp_event import ( + SyncSmartAppEventResponsePayload, +) from pybotx.client.stickers_api.exceptions import ( InvalidEmojiError, InvalidImageError, @@ -218,7 +226,7 @@ "RequestHeadersNotProvidedError", "SmartApp", "SmartAppEvent", - "SmartAppEvent", + "SyncSmartAppEventResponsePayload", "SmartappManifest", "SmartappManifestWebLayoutChoices", "SmartappManifestWebParams", @@ -227,6 +235,8 @@ "Sticker", "StickerPack", "StickerPackOrStickerNotFoundError", + "SyncSmartAppEventHandlerFunc", + "SyncSmartAppEventHandlerNotFoundError", "SyncSourceTypes", "UnknownBotAccountError", "UnknownSystemEventError", diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 72375532..ca563512 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -148,6 +148,9 @@ BotXAPISmartAppsListRequestPayload, SmartAppsListMethod, ) +from pybotx.client.smartapps_api.sync_smartapp_event import ( + SyncSmartAppEventResponsePayload, +) from pybotx.client.smartapps_api.upload_file import ( UploadFileMethod as SmartappsUploadFileMethod, ) @@ -242,6 +245,10 @@ build_bot_status_response, ) from pybotx.models.stickers import Sticker, StickerPack, StickerPackFromList +from pybotx.models.system_events.smartapp_event import ( + BotAPISmartAppEvent, + SmartAppEvent, +) from pybotx.models.users import UserFromCSV, UserFromSearch MissingOptionalAttachment = MissingOptional[ @@ -325,6 +332,49 @@ def async_execute_bot_command( return self._handler_collector.async_handle_bot_command(self, bot_command) + async def sync_execute_raw_smartapp_event( + self, + raw_smartapp_event: Dict[str, Any], + verify_request: bool = True, + request_headers: Optional[Mapping[str, str]] = None, + logging_command: bool = True, + ) -> SyncSmartAppEventResponsePayload: + if logging_command: + logger.opt(lazy=True).debug( + "Got sync smartapp event: {command}", + command=lambda: pformat_jsonable_obj( + trim_file_data_in_incoming_json(raw_smartapp_event), + ), + ) + + if verify_request: + if request_headers is None: + raise RequestHeadersNotProvidedError + self._verify_request(request_headers) + + try: + bot_api_smartapp_event: BotAPISmartAppEvent = parse_obj_as( + BotAPISmartAppEvent, + raw_smartapp_event, + ) + except ValidationError as validation_exc: + raise ValueError( + "Sync smartapp event validation error", + ) from validation_exc + + smartapp_event = bot_api_smartapp_event.to_domain(raw_smartapp_event) + return await self.sync_execute_smartapp_event(smartapp_event) + + async def sync_execute_smartapp_event( + self, + smartapp_event: SmartAppEvent, + ) -> SyncSmartAppEventResponsePayload: + self._bot_accounts_storage.ensure_bot_id_exists(smartapp_event.bot.id) + return await self._handler_collector.handle_sync_smartapp_event( + self, + smartapp_event, + ) + async def raw_get_status( self, query_params: Dict[str, str], diff --git a/pybotx/bot/handler.py b/pybotx/bot/handler.py index f2eedc99..206b8523 100644 --- a/pybotx/bot/handler.py +++ b/pybotx/bot/handler.py @@ -2,6 +2,9 @@ from functools import partial from typing import TYPE_CHECKING, Awaitable, Callable, List, Literal, TypeVar, Union +from pybotx.client.smartapps_api.sync_smartapp_event import ( + SyncSmartAppEventResponsePayload, +) from pybotx.models.commands import BotCommand from pybotx.models.message.incoming_message import IncomingMessage from pybotx.models.status import StatusRecipient @@ -23,6 +26,11 @@ TBotCommand = TypeVar("TBotCommand", bound=BotCommand) HandlerFunc = Callable[[TBotCommand, "Bot"], Awaitable[None]] +SyncSmartAppEventHandlerFunc = Callable[ + [SmartAppEvent, "Bot"], + Awaitable[SyncSmartAppEventResponsePayload], +] + IncomingMessageHandlerFunc = HandlerFunc[IncomingMessage] SystemEventHandlerFunc = Union[ HandlerFunc[AddedToChatEvent], diff --git a/pybotx/bot/handler_collector.py b/pybotx/bot/handler_collector.py index 42ce36d5..411a7b6f 100644 --- a/pybotx/bot/handler_collector.py +++ b/pybotx/bot/handler_collector.py @@ -2,11 +2,13 @@ import re from typing import ( TYPE_CHECKING, + Any, Callable, Dict, List, Optional, Sequence, + Set, Type, Union, overload, @@ -21,6 +23,7 @@ HiddenCommandHandler, IncomingMessageHandlerFunc, Middleware, + SyncSmartAppEventHandlerFunc, SystemEventHandlerFunc, VisibleCommandHandler, VisibleFunc, @@ -29,6 +32,10 @@ ExceptionHandlersDict, ExceptionMiddleware, ) +from pybotx.client.smartapps_api.exceptions import SyncSmartAppEventHandlerNotFoundError +from pybotx.client.smartapps_api.sync_smartapp_event import ( + SyncSmartAppEventResponsePayload, +) from pybotx.converters import optional_sequence_to_list from pybotx.logger import logger from pybotx.models.commands import BotCommand, SystemEvent @@ -60,6 +67,10 @@ def __init__(self, middlewares: Optional[Sequence[Middleware]] = None) -> None: Type[BotCommand], SystemEventHandlerFunc, ] = {} + self._sync_smartapp_event_handler: Dict[ + Type[SmartAppEvent], + SyncSmartAppEventHandlerFunc, + ] = {} self._middlewares = optional_sequence_to_list(middlewares) self._tasks: "WeakSet[asyncio.Task[None]]" = WeakSet() @@ -111,6 +122,26 @@ async def handle_bot_command(self, bot_command: BotCommand, bot: "Bot") -> None: else: raise NotImplementedError(f"Unsupported event type: `{bot_command}`") + async def handle_sync_smartapp_event( + self, + bot: "Bot", + smartapp_event: SmartAppEvent, + ) -> SyncSmartAppEventResponsePayload: + if not isinstance(smartapp_event, SmartAppEvent): + raise NotImplementedError( + f"Unsupported event type for sync smartapp event: `{smartapp_event}`", + ) + + event_handler = self._get_sync_smartapp_event_handler_or_none(smartapp_event) + + if not event_handler: + raise SyncSmartAppEventHandlerNotFoundError( + "Handler for sync smartapp event not found", + ) + + self._fill_contextvars(smartapp_event, bot) + return await event_handler(smartapp_event, bot) + async def get_bot_menu( self, status_recipient: StatusRecipient, @@ -291,6 +322,14 @@ def smartapp_event( return handler_func + def sync_smartapp_event( + self, + handler_func: SyncSmartAppEventHandlerFunc, + ) -> SyncSmartAppEventHandlerFunc: + """Decorate `smartapp` sync event handler.""" + self._sync_smartapp_event(SmartAppEvent, handler_func) + return handler_func + def insert_exception_middleware( self, exception_handlers: Optional[ExceptionHandlersDict] = None, @@ -305,7 +344,7 @@ async def wait_active_tasks(self) -> None: return_when=asyncio.ALL_COMPLETED, ) - def _include_collector(self, other: "HandlerCollector") -> None: + def _include_collector(self, other: "HandlerCollector") -> None: # noqa: WPS238 # - Message handlers - command_duplicates = set(self._user_commands_handlers) & set( other._user_commands_handlers, @@ -340,6 +379,19 @@ def _include_collector(self, other: "HandlerCollector") -> None: self._system_events_handlers.update(other._system_events_handlers) + # - Sync smartapp event handler - + sync_events_duplicates: Set[Type[SmartAppEvent]] = set( + self._sync_smartapp_event_handler, + ) & set( + other._sync_smartapp_event_handler, + ) + if sync_events_duplicates: + raise ValueError( + "Handler for sync smartapp event already registered", + ) + + self._sync_smartapp_event_handler.update(other._sync_smartapp_event_handler) + def _get_incoming_message_handler( self, message: IncomingMessage, @@ -377,6 +429,17 @@ def _get_system_event_handler_or_none( return handler + def _get_sync_smartapp_event_handler_or_none( + self, + event: SmartAppEvent, + ) -> Optional[SyncSmartAppEventHandlerFunc]: + event_cls = event.__class__ + + handler = self._sync_smartapp_event_handler.get(event_cls) + self._log_system_event_handler_call(event_cls.__name__, handler) + + return handler + def _get_command_name(self, body: str) -> Optional[str]: if not body: return None @@ -422,6 +485,18 @@ def _system_event( return handler_func + def _sync_smartapp_event( + self, + event_cls_name: Type[SmartAppEvent], + handler_func: SyncSmartAppEventHandlerFunc, + ) -> SyncSmartAppEventHandlerFunc: + if event_cls_name in self._sync_smartapp_event_handler: + raise ValueError("Handler for sync smartapp event already registered") + + self._sync_smartapp_event_handler[event_cls_name] = handler_func + + return handler_func + def _fill_contextvars(self, bot_command: BotCommand, bot: "Bot") -> None: bot_var.set(bot) bot_id_var.set(bot_command.bot.id) @@ -433,7 +508,7 @@ def _fill_contextvars(self, bot_command: BotCommand, bot: "Bot") -> None: def _log_system_event_handler_call( self, event_cls_name: str, - handler: Optional[SystemEventHandlerFunc], + handler: Any, ) -> None: if handler: logger.info(f"Found handler for `{event_cls_name}`") diff --git a/pybotx/client/smartapps_api/exceptions.py b/pybotx/client/smartapps_api/exceptions.py new file mode 100644 index 00000000..2c129f1c --- /dev/null +++ b/pybotx/client/smartapps_api/exceptions.py @@ -0,0 +1,5 @@ +from pybotx.client.exceptions.base import BaseClientError + + +class SyncSmartAppEventHandlerNotFoundError(BaseClientError): + """Handler for synchronous smartapp event not found.""" diff --git a/pybotx/client/smartapps_api/smartapp_event.py b/pybotx/client/smartapps_api/smartapp_event.py index e8649ac5..abf7c61f 100644 --- a/pybotx/client/smartapps_api/smartapp_event.py +++ b/pybotx/client/smartapps_api/smartapp_event.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Literal +from typing import Any, Dict, List, Literal, Type, TypeVar from uuid import UUID from pybotx.client.authorized_botx_method import AuthorizedBotXMethod @@ -7,6 +7,11 @@ from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel from pybotx.models.async_files import APIAsyncFile, File, convert_async_file_from_domain +TBotXAPISmartAppEventRequestPayload = TypeVar( + "TBotXAPISmartAppEventRequestPayload", + bound="BotXAPISmartAppEventRequestPayload", +) + class BotXAPISmartAppEventRequestPayload(UnverifiedPayloadBaseModel): ref: MissingOptional[UUID] @@ -20,7 +25,7 @@ class BotXAPISmartAppEventRequestPayload(UnverifiedPayloadBaseModel): @classmethod def from_domain( - cls, + cls: Type[TBotXAPISmartAppEventRequestPayload], ref: MissingOptional[UUID], smartapp_id: UUID, chat_id: UUID, @@ -28,7 +33,7 @@ def from_domain( opts: Missing[Dict[str, Any]], files: Missing[List[File]], encrypted: bool, - ) -> "BotXAPISmartAppEventRequestPayload": + ) -> TBotXAPISmartAppEventRequestPayload: api_async_files: Missing[List[APIAsyncFile]] = Undefined if files: api_async_files = [convert_async_file_from_domain(file) for file in files] diff --git a/pybotx/client/smartapps_api/sync_smartapp_event.py b/pybotx/client/smartapps_api/sync_smartapp_event.py new file mode 100644 index 00000000..99cd788d --- /dev/null +++ b/pybotx/client/smartapps_api/sync_smartapp_event.py @@ -0,0 +1,7 @@ +from pybotx.client.smartapps_api.smartapp_event import ( + BotXAPISmartAppEventRequestPayload, +) + + +class SyncSmartAppEventResponsePayload(BotXAPISmartAppEventRequestPayload): + """The response payload to a synchronous smartapp event at /smartapps/request.""" diff --git a/pyproject.toml b/pyproject.toml index e8894e90..eec606cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.66.1" +version = "0.67.0" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", @@ -23,9 +23,7 @@ httpx = "^0.25.0" # https://github.com/encode/httpcore/pull/880 httpcore = ">=1.0.0,<1.0.3" loguru = ">=0.6.0,<0.7.0" -mypy-extensions = ">=0.2.0,<0.5.0" pydantic = ">=1.6.0,<1.11.0" -typing-extensions = ">=3.7.4,<5.0.0" aiocsv = ">=1.2.3,<1.3.0" pyjwt = ">=2.0.0,<3.0.0" @@ -35,6 +33,8 @@ autoflake = "1.7.8" black = "22.3.0" isort = "5.10.1" mypy = "0.910.0" +mypy-extensions = ">=0.2.0,<0.5.0" +typing-extensions = ">=3.7.4,<5.0.0" wemake-python-styleguide = "0.16.0" bandit = "1.7.2" # https://github.com/PyCQA/bandit/issues/837 diff --git a/setup.cfg b/setup.cfg index 25f477c2..bb108bcf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,8 @@ per-file-ignores = pybotx/constants.py:WPS432, pybotx/__init__.py:WPS203,WPS410,WPS412,F401, # https://github.com/wemake-services/wemake-python-styleguide/issues/2172 - pybotx/bot/handler_collector.py:WPS437, + pybotx/bot/handler_collector.py:WPS226,WPS437, + pybotx/bot/handler.py:WPS226, pybotx/client/notifications_api/internal_bot_notification.py:WPS202, pybotx/client/smartapps_api/smartapp_custom_notification.py:WPS118, # Complex model converting @@ -70,7 +71,7 @@ per-file-ignores = # Allow using methods names with trailing underscore pybotx/models/enums.py:WPS120 - tests/*:DAR101,E501,WPS110,WPS114,WPS116,WPS118,WPS202,WPS221,WPS226,WPS237,WPS402,WPS420,WPS428,WPS430,WPS432,WPS441,WPS442,WPS520,PT011,S105,S106,WPS437,WPS609 + tests/*:DAR101,E501,WPS110,WPS114,WPS116,WPS118,WPS202,WPS221,WPS226,WPS237,WPS402,WPS420,WPS428,WPS430,WPS432,WPS441,WPS442,WPS520,PT011,S105,S106,WPS437,WPS609,WPS231 # Import ignores for README lint .snippets/*:F403,F405,WPS347,WPS421,S106,WPS237 diff --git a/tests/conftest.py b/tests/conftest.py index 8e47dde9..9e2d1eeb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,11 +13,15 @@ from respx.router import MockRouter from pybotx import ( + Bot, BotAccount, BotAccountWithSecret, Chat, ChatTypes, + HandlerCollector, IncomingMessage, + SmartAppEvent, + SyncSmartAppEventResponsePayload, UserDevice, UserSender, ) @@ -224,6 +228,74 @@ def decorator( return decorator +@pytest.fixture +def api_sync_smartapp_event_factory() -> Callable[..., Dict[str, Any]]: + def decorator( + *, + bot_id: Optional[UUID] = None, + group_chat_id: Optional[UUID] = None, + user_huid: Optional[UUID] = None, + host: Optional[str] = None, + attachment: Optional[Dict[str, Any]] = None, + async_file: Optional[Dict[str, Any]] = None, + method: Optional[str] = None, + params: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + return { + "sync_id": "a465f0f3-1354-491c-8f11-f400164295cb", + "command": { + "body": "system:smartapp_event", + "data": { + "ref": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + "smartapp_id": str(bot_id) + if bot_id + else "8dada2c8-67a6-4434-9dec-570d244e78ee", + "data": { + "type": "smartapp_rpc", + "method": method or "folders.get", + "params": params or {}, + }, + "opts": {"option": "test_option"}, + "smartapp_api_version": 1, + }, + "command_type": "system", + "metadata": {}, + }, + "async_files": [async_file] if async_file else [], + "attachments": [attachment] if attachment else [], + "entities": [], + "from": { + "user_huid": str(user_huid) + if user_huid + else "b9197d3a-d855-5d34-ba8a-eff3a975ab20", + "user_udid": None, + "group_chat_id": str(group_chat_id) + if group_chat_id + else "dea55ee4-7a9f-5da0-8c73-079f400ee517", + "host": host or "cts.example.com", + "ad_login": None, + "ad_domain": None, + "username": None, + "chat_type": "group_chat", + "manufacturer": None, + "device": None, + "device_software": None, + "device_meta": {}, + "platform": None, + "platform_package_id": None, + "is_admin": False, + "is_creator": False, + "app_version": None, + "locale": "en", + }, + "bot_id": str(bot_id) if bot_id else "8dada2c8-67a6-4434-9dec-570d244e78ee", + "proto_version": 4, + "source_sync_id": None, + } + + return decorator + + @pytest.fixture def incoming_message_factory( bot_id: UUID, @@ -288,3 +360,67 @@ def incorrect_handler_trigger() -> Mock: @pytest.fixture(autouse=True) def prevent_http_requests(respx_mock: MockRouter) -> None: pass + + +@pytest.fixture +def collector_with_sync_smartapp_event_handler() -> HandlerCollector: + collector = HandlerCollector() + + @collector.sync_smartapp_event + async def handle_sync_smartapp_event( + event: SmartAppEvent, + _: Bot, + ) -> SyncSmartAppEventResponsePayload: + return SyncSmartAppEventResponsePayload.from_domain( + ref=event.ref, + smartapp_id=event.bot.id, + chat_id=event.chat.id, + data=event.data, + opts={}, + files=event.files, + encrypted=True, + ) + + return collector + + +@pytest.fixture +def smartapp_event(bot_id: UUID, host: str) -> SmartAppEvent: + return SmartAppEvent( + bot=BotAccount( + id=bot_id, + host=host, + ), + raw_command=None, + ref=uuid4(), + smartapp_id=bot_id, + data={}, + opts={}, + smartapp_api_version=1, + files=[], + chat=Chat( + id=uuid4(), + type=ChatTypes.PERSONAL_CHAT, + ), + sender=UserSender( + huid=uuid4(), + udid=None, + ad_login=None, + ad_domain=None, + username=None, + is_chat_admin=True, + is_chat_creator=True, + device=UserDevice( + manufacturer=None, + device_name=None, + os=None, + pushes=None, + timezone=None, + permissions=None, + platform=None, + platform_package_id=None, + app_version=None, + locale=None, + ), + ), + ) diff --git a/tests/test_base_command.py b/tests/test_base_command.py index 3a8d2600..5de76535 100644 --- a/tests/test_base_command.py +++ b/tests/test_base_command.py @@ -8,6 +8,7 @@ Bot, BotAccountWithSecret, HandlerCollector, + RequestHeadersNotProvidedError, UnknownSystemEventError, UnsupportedBotAPIVersionError, lifespan_wrapper, @@ -143,3 +144,120 @@ async def test__async_execute_raw_bot_command__not_logging_incoming_request( # - Assert - assert log_message not in loguru_caplog.messages + + +async def test__sync_execute_raw_smartapp_event__logging_incoming_request( + bot_account: BotAccountWithSecret, + loguru_caplog: pytest.LogCaptureFixture, + api_sync_smartapp_event_factory: Callable[..., Dict[str, Any]], + collector_with_sync_smartapp_event_handler: HandlerCollector, +) -> None: + # - Arrange - + payload = api_sync_smartapp_event_factory(bot_id=bot_account.id) + log_message = "Got sync smartapp event: {command}".format( + command=json.dumps(payload, sort_keys=True, indent=4, ensure_ascii=False), + ) + built_bot = Bot( + collectors=[collector_with_sync_smartapp_event_handler], + bot_accounts=[bot_account], + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with loguru_caplog.at_level(logging.DEBUG): + await bot.sync_execute_raw_smartapp_event(payload, verify_request=False) + + # - Assert - + assert log_message in loguru_caplog.messages + + +async def test__sync_execute_raw_smartapp_event__not_logging_incoming_request( + bot_account: BotAccountWithSecret, + loguru_caplog: pytest.LogCaptureFixture, + api_sync_smartapp_event_factory: Callable[..., Dict[str, Any]], + collector_with_sync_smartapp_event_handler: HandlerCollector, +) -> None: + # - Arrange - + payload = api_sync_smartapp_event_factory(bot_id=bot_account.id) + log_message = "Got sync smartapp event: {command}".format( + command=json.dumps(payload, sort_keys=True, indent=4, ensure_ascii=False), + ) + built_bot = Bot( + collectors=[collector_with_sync_smartapp_event_handler], + bot_accounts=[bot_account], + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with loguru_caplog.at_level(logging.DEBUG): + bot.async_execute_raw_bot_command( + payload, + verify_request=False, + logging_command=False, + ) + + # - Assert - + assert log_message not in loguru_caplog.messages + + +async def test__sync_execute_raw_smartapp_event__headers_not_provided( + bot_account: BotAccountWithSecret, + api_sync_smartapp_event_factory: Callable[..., Dict[str, Any]], + collector_with_sync_smartapp_event_handler: HandlerCollector, +) -> None: + # - Arrange - + payload = api_sync_smartapp_event_factory(bot_id=bot_account.id) + built_bot = Bot( + collectors=[collector_with_sync_smartapp_event_handler], + bot_accounts=[bot_account], + ) + + # - Act and Assert- + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(RequestHeadersNotProvidedError): + await bot.sync_execute_raw_smartapp_event(payload, verify_request=True) + + +async def test__sync_execute_raw_smartapp_event__request_verified( + bot_account: BotAccountWithSecret, + api_sync_smartapp_event_factory: Callable[..., Dict[str, Any]], + collector_with_sync_smartapp_event_handler: HandlerCollector, + authorization_header: Dict[str, str], +) -> None: + # - Arrange - + payload = api_sync_smartapp_event_factory(bot_id=bot_account.id) + built_bot = Bot( + collectors=[collector_with_sync_smartapp_event_handler], + bot_accounts=[bot_account], + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + response = await bot.sync_execute_raw_smartapp_event( + payload, + verify_request=True, + request_headers=authorization_header, + ) + + assert response + + +async def test__sync_execute_raw_smartapp_event__incorrect_payload( + bot_account: BotAccountWithSecret, + collector_with_sync_smartapp_event_handler: HandlerCollector, +) -> None: + # - Arrange - + payload = {"incorrect": "payload"} + built_bot = Bot( + collectors=[collector_with_sync_smartapp_event_handler], + bot_accounts=[bot_account], + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(ValueError): + await bot.sync_execute_raw_smartapp_event( + payload, + verify_request=False, + logging_command=False, + ) diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 6ecf1ae4..442e64a8 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -15,6 +15,8 @@ BotAccountWithSecret, HandlerCollector, IncomingMessage, + SmartAppEvent, + SyncSmartAppEventResponsePayload, UnknownBotAccountError, UnverifiedRequestError, build_bot_disabled_response, @@ -33,6 +35,22 @@ async def debug_handler(message: IncomingMessage, bot: Bot) -> None: await bot.answer_message("Hi!") +@collector.sync_smartapp_event +async def handle_sync_smartapp_event( + event: SmartAppEvent, + _: Bot, +) -> SyncSmartAppEventResponsePayload: + return SyncSmartAppEventResponsePayload.from_domain( + ref=event.ref, + smartapp_id=event.bot.id, + chat_id=event.chat.id, + data=event.data, + opts={}, + files=event.files, + encrypted=True, + ) + + def bot_factory( bot_accounts: List[BotAccountWithSecret], ) -> Bot: @@ -81,6 +99,36 @@ async def command_handler( ) +@router.post("/smartapps/request") +async def sync_smartapp_event_handler( + request: Request, + bot: Bot = bot_dependency, +) -> JSONResponse: + try: + response = await bot.sync_execute_raw_smartapp_event( + await request.json(), + verify_request=False, + ) + except ValueError: + error_label = "Bot command validation error" + logger.exception(error_label) + + return JSONResponse( + build_bot_disabled_response(error_label), + status_code=HTTPStatus.SERVICE_UNAVAILABLE, + ) + except UnknownBotAccountError as exc: + error_label = f"No credentials for bot {exc.bot_id}" + logger.warning(error_label) + + return JSONResponse( + build_bot_disabled_response(error_label), + status_code=HTTPStatus.SERVICE_UNAVAILABLE, + ) + + return JSONResponse(response.jsonable_dict(), status_code=HTTPStatus.OK) + + @router.get("/status") async def status_handler(request: Request, bot: Bot = bot_dependency) -> JSONResponse: status = await bot.raw_get_status(dict(request.query_params), verify_request=False) @@ -367,3 +415,99 @@ def test__web_app__unverified_request_response( "errors": [], "reason": "unverified_request", } + + +def test__web_app__sync_smartapp_event(bot: Bot) -> None: + # - Arrange - + request_payload = { + "sync_id": "a465f0f3-1354-491c-8f11-f400164295cb", + "command": { + "body": "system:smartapp_event", + "data": { + "ref": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + "smartapp_id": "8dada2c8-67a6-4434-9dec-570d244e78ee", + "data": { + "type": "smartapp_rpc", + "method": "folders.get", + "params": { + "q": 1, + }, + }, + "opts": {"option": "test_option"}, + "smartapp_api_version": 1, + }, + "command_type": "system", + "metadata": {}, + }, + "async_files": [ + { + "type": "image", + "file": "https://link.to/file", + "file_mime_type": "image/png", + "file_name": "pass.png", + "file_preview": "https://link.to/preview", + "file_preview_height": 300, + "file_preview_width": 300, + "file_size": 1502345, + "file_hash": "Jd9r+OKpw5y+FSCg1xNTSUkwEo4nCW1Sn1AkotkOpH0=", + "file_encryption_algo": "stream", + "chunk_size": 2097152, + "file_id": "8dada2c8-67a6-4434-9dec-570d244e78ee", + }, + ], + "attachments": [], + "entities": [], + "from": { + "user_huid": "b9197d3a-d855-5d34-ba8a-eff3a975ab20", + "user_udid": None, + "group_chat_id": "dea55ee4-7a9f-5da0-8c73-079f400ee517", + "host": "cts.example.com", + "ad_login": None, + "ad_domain": None, + "username": None, + "chat_type": "group_chat", + "manufacturer": None, + "device": None, + "device_software": None, + "device_meta": {}, + "platform": None, + "platform_package_id": None, + "is_admin": False, + "is_creator": False, + "app_version": None, + "locale": "en", + }, + "bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46", + "proto_version": 4, + "source_sync_id": None, + } + + # - Act - + with TestClient(fastapi_factory(bot)) as test_client: + response = test_client.post( + "/smartapps/request", + json=request_payload, + ) + + # - Assert - + assert response.status_code == HTTPStatus.OK + assert response.json() == { + "ref": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + "smartapp_id": "24348246-6791-4ac0-9d86-b948cd6a0e46", + "group_chat_id": "dea55ee4-7a9f-5da0-8c73-079f400ee517", + "data": {"type": "smartapp_rpc", "method": "folders.get", "params": {"q": 1}}, + "opts": {}, + "smartapp_api_version": 1, + "async_files": [ + { + "type": "image", + "file": "https://link.to/file", + "file_mime_type": "image/png", + "file_id": "8dada2c8-67a6-4434-9dec-570d244e78ee", + "file_name": "pass.png", + "file_size": 1502345, + "file_hash": "Jd9r+OKpw5y+FSCg1xNTSUkwEo4nCW1Sn1AkotkOpH0=", + }, + ], + "encrypted": True, + } diff --git a/tests/test_handler_collector.py b/tests/test_handler_collector.py index 8e672c7f..5d86cf73 100644 --- a/tests/test_handler_collector.py +++ b/tests/test_handler_collector.py @@ -1,4 +1,5 @@ -from typing import Callable +from copy import deepcopy +from typing import Any, Callable from unittest.mock import Mock import pytest @@ -9,6 +10,8 @@ ChatCreatedEvent, HandlerCollector, IncomingMessage, + SmartAppEvent, + SyncSmartAppEventHandlerNotFoundError, lifespan_wrapper, ) @@ -507,3 +510,53 @@ async def handler(message: IncomingMessage, bot: Bot) -> None: # - Assert - correct_handler_trigger.assert_called_once() + + +@pytest.mark.asyncio +async def test__handler_collector__handle_sync_smartapp_event__handler_not_found( + bot_account: BotAccountWithSecret, + smartapp_event: SmartAppEvent, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act and Assert - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(SyncSmartAppEventHandlerNotFoundError): + await collector.handle_sync_smartapp_event( + bot, + smartapp_event=smartapp_event, + ) + + +@pytest.mark.asyncio +async def test__handler_collector__sync_smartapp_event__include__handler_already_registered( + collector_with_sync_smartapp_event_handler: HandlerCollector, +) -> None: + # - Arrange - + collector1 = collector_with_sync_smartapp_event_handler + collector2 = deepcopy(collector_with_sync_smartapp_event_handler) + + # - Act and Assert - + with pytest.raises(ValueError) as exc: + collector1.include(collector2) + + assert str(exc.value) == "Handler for sync smartapp event already registered" + + +@pytest.mark.asyncio +async def test__handler_collector__sync_smartapp_event__decorator__handler_already_registered( + collector_with_sync_smartapp_event_handler: HandlerCollector, +) -> None: + # - Arrange - + collector = collector_with_sync_smartapp_event_handler + + # - Act and Assert - + with pytest.raises(ValueError) as exc: + + @collector.sync_smartapp_event + async def duplicated_handle_sync_smartapp_event(*_: Any) -> Any: + ... + + assert str(exc.value) == "Handler for sync smartapp event already registered"