From fb09734601fa0b0e9f1d05072c8b152374446be8 Mon Sep 17 00:00:00 2001 From: Aleksander Maksimeniuk <78569692+alexhook@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:47:38 +0300 Subject: [PATCH] feat: added incoming requests verification using authorization JWT tokens (#447) --- README.md | 15 +- poetry.lock | 420 +++++++++++------ pybotx/__init__.py | 22 +- pybotx/bot/api/responses/bot_disabled.py | 2 +- .../bot/api/responses/unverified_request.py | 39 ++ pybotx/bot/bot.py | 77 ++- pybotx/bot/bot_accounts_storage.py | 20 +- pybotx/bot/exceptions.py | 11 + pyproject.toml | 7 +- setup.cfg | 2 +- .../test_direct_notification.py | 14 +- .../test_internal_bot_notification.py | 4 + tests/client/notifications_api/test_markup.py | 3 + .../test_smartapp_custom_notification.py | 1 + tests/client/test_botx_method_callback.py | 12 +- tests/conftest.py | 25 + tests/system_events/test_added_to_chat.py | 2 +- tests/system_events/test_chat_created.py | 2 +- tests/system_events/test_cts_login.py | 2 +- tests/system_events/test_cts_logout.py | 2 +- tests/system_events/test_deleted_from_chat.py | 2 +- tests/system_events/test_event_edit.py | 2 +- .../test_internal_bot_notification.py | 2 +- tests/system_events/test_left_from_chat.py | 2 +- tests/system_events/test_smartapp_event.py | 2 +- tests/test_attachments.py | 10 +- tests/test_base_command.py | 14 +- tests/test_end_to_end.py | 49 +- tests/test_files.py | 4 +- tests/test_incoming_message.py | 10 +- tests/test_logs.py | 2 +- tests/test_status.py | 16 +- tests/test_stickers.py | 2 +- tests/test_verify_request.py | 437 ++++++++++++++++++ 34 files changed, 1020 insertions(+), 216 deletions(-) create mode 100644 pybotx/bot/api/responses/unverified_request.py create mode 100644 tests/test_verify_request.py diff --git a/README.md b/README.md index ccbcfd3f..e676f861 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,10 @@ app.add_event_handler("shutdown", bot.shutdown) # (сообщения и системные события). @app.post("/command") async def command_handler(request: Request) -> JSONResponse: - bot.async_execute_raw_bot_command(await request.json()) + bot.async_execute_raw_bot_command( + await request.json(), + request_headers=request.headers, + ) return JSONResponse( build_command_accepted_response(), status_code=HTTPStatus.ACCEPTED, @@ -114,7 +117,10 @@ async def command_handler(request: Request) -> JSONResponse: # доступность бота и его список команд. @app.get("/status") async def status_handler(request: Request) -> JSONResponse: - status = await bot.raw_get_status(dict(request.query_params)) + status = await bot.raw_get_status( + dict(request.query_params), + request_headers=request.headers, + ) return JSONResponse(status) @@ -122,7 +128,10 @@ async def status_handler(request: Request) -> JSONResponse: # выполнения асинхронных методов в BotX. @app.post("/notification/callback") async def callback_handler(request: Request) -> JSONResponse: - await bot.set_raw_botx_method_result(await request.json()) + await bot.set_raw_botx_method_result( + await request.json(), + verify_request=False, + ) return JSONResponse( build_command_accepted_response(), status_code=HTTPStatus.ACCEPTED, diff --git a/poetry.lock b/poetry.lock index 5ef23701..1d1defb0 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 = "0.8.0" description = "File support for asyncio." +category = "main" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -61,29 +64,32 @@ files = [ [[package]] name = "anyio" -version = "4.0.0" +version = "4.3.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, - {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] [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)"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.22)"] +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 = "asgiref" version = "3.7.2" description = "ASGI specs, helper code, and adapters" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -101,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 = [ @@ -110,26 +117,29 @@ files = [ [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +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-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +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" optional = false python-versions = ">=3.7" files = [ @@ -145,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 = [ @@ -167,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 = [ @@ -211,19 +223,21 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[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 = [ @@ -323,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 = [ @@ -337,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 = [ @@ -346,63 +362,64 @@ files = [ [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.2" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf54c3e089179d9d23900e3efc86d46e4431188d9a657f345410eecdd0151f50"}, + {file = "coverage-7.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe6e43c8b510719b48af7db9631b5fbac910ade4bd90e6378c85ac5ac706382c"}, + {file = "coverage-7.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b98c89db1b150d851a7840142d60d01d07677a18f0f46836e691c38134ed18b"}, + {file = "coverage-7.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5f9683be6a5b19cd776ee4e2f2ffb411424819c69afab6b2db3a0a364ec6642"}, + {file = "coverage-7.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cdcbf7b9cb83fe047ee09298e25b1cd1636824067166dc97ad0543b079d22f"}, + {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2599972b21911111114100d362aea9e70a88b258400672626efa2b9e2179609c"}, + {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ef00d31b7569ed3cb2036f26565f1984b9fc08541731ce01012b02a4c238bf03"}, + {file = "coverage-7.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:20a875bfd8c282985c4720c32aa05056f77a68e6d8bbc5fe8632c5860ee0b49b"}, + {file = "coverage-7.4.2-cp310-cp310-win32.whl", hash = "sha256:b3f2b1eb229f23c82898eedfc3296137cf1f16bb145ceab3edfd17cbde273fb7"}, + {file = "coverage-7.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7df95fdd1432a5d2675ce630fef5f239939e2b3610fe2f2b5bf21fa505256fa3"}, + {file = "coverage-7.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8ddbd158e069dded57738ea69b9744525181e99974c899b39f75b2b29a624e2"}, + {file = "coverage-7.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81a5fb41b0d24447a47543b749adc34d45a2cf77b48ca74e5bf3de60a7bd9edc"}, + {file = "coverage-7.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2412e98e70f16243be41d20836abd5f3f32edef07cbf8f407f1b6e1ceae783ac"}, + {file = "coverage-7.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb79414c15c6f03f56cc68fa06994f047cf20207c31b5dad3f6bab54a0f66ef"}, + {file = "coverage-7.4.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf89ab85027427d351f1de918aff4b43f4eb5f33aff6835ed30322a86ac29c9e"}, + {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a178b7b1ac0f1530bb28d2e51f88c0bab3e5949835851a60dda80bff6052510c"}, + {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:06fe398145a2e91edaf1ab4eee66149c6776c6b25b136f4a86fcbbb09512fd10"}, + {file = "coverage-7.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:18cac867950943fe93d6cd56a67eb7dcd2d4a781a40f4c1e25d6f1ed98721a55"}, + {file = "coverage-7.4.2-cp311-cp311-win32.whl", hash = "sha256:f72cdd2586f9a769570d4b5714a3837b3a59a53b096bb954f1811f6a0afad305"}, + {file = "coverage-7.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:d779a48fac416387dd5673fc5b2d6bd903ed903faaa3247dc1865c65eaa5a93e"}, + {file = "coverage-7.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:adbdfcda2469d188d79771d5696dc54fab98a16d2ef7e0875013b5f56a251047"}, + {file = "coverage-7.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac4bab32f396b03ebecfcf2971668da9275b3bb5f81b3b6ba96622f4ef3f6e17"}, + {file = "coverage-7.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:006d220ba2e1a45f1de083d5022d4955abb0aedd78904cd5a779b955b019ec73"}, + {file = "coverage-7.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3733545eb294e5ad274abe131d1e7e7de4ba17a144505c12feca48803fea5f64"}, + {file = "coverage-7.4.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42a9e754aa250fe61f0f99986399cec086d7e7a01dd82fd863a20af34cbce962"}, + {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2ed37e16cf35c8d6e0b430254574b8edd242a367a1b1531bd1adc99c6a5e00fe"}, + {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b953275d4edfab6cc0ed7139fa773dfb89e81fee1569a932f6020ce7c6da0e8f"}, + {file = "coverage-7.4.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32b4ab7e6c924f945cbae5392832e93e4ceb81483fd6dc4aa8fb1a97b9d3e0e1"}, + {file = "coverage-7.4.2-cp312-cp312-win32.whl", hash = "sha256:f5df76c58977bc35a49515b2fbba84a1d952ff0ec784a4070334dfbec28a2def"}, + {file = "coverage-7.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:34423abbaad70fea9d0164add189eabaea679068ebdf693baa5c02d03e7db244"}, + {file = "coverage-7.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b11f9c6587668e495cc7365f85c93bed34c3a81f9f08b0920b87a89acc13469"}, + {file = "coverage-7.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:51593a1f05c39332f623d64d910445fdec3d2ac2d96b37ce7f331882d5678ddf"}, + {file = "coverage-7.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69f1665165ba2fe7614e2f0c1aed71e14d83510bf67e2ee13df467d1c08bf1e8"}, + {file = "coverage-7.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3c8bbb95a699c80a167478478efe5e09ad31680931ec280bf2087905e3b95ec"}, + {file = "coverage-7.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:175f56572f25e1e1201d2b3e07b71ca4d201bf0b9cb8fad3f1dfae6a4188de86"}, + {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8562ca91e8c40864942615b1d0b12289d3e745e6b2da901d133f52f2d510a1e3"}, + {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d9a1ef0f173e1a19738f154fb3644f90d0ada56fe6c9b422f992b04266c55d5a"}, + {file = "coverage-7.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f40ac873045db4fd98a6f40387d242bde2708a3f8167bd967ccd43ad46394ba2"}, + {file = "coverage-7.4.2-cp38-cp38-win32.whl", hash = "sha256:d1b750a8409bec61caa7824bfd64a8074b6d2d420433f64c161a8335796c7c6b"}, + {file = "coverage-7.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b4ae777bebaed89e3a7e80c4a03fac434a98a8abb5251b2a957d38fe3fd30088"}, + {file = "coverage-7.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ff7f92ae5a456101ca8f48387fd3c56eb96353588e686286f50633a611afc95"}, + {file = "coverage-7.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:861d75402269ffda0b33af94694b8e0703563116b04c681b1832903fac8fd647"}, + {file = "coverage-7.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3507427d83fa961cbd73f11140f4a5ce84208d31756f7238d6257b2d3d868405"}, + {file = "coverage-7.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf711d517e21fb5bc429f5c4308fbc430a8585ff2a43e88540264ae87871e36a"}, + {file = "coverage-7.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c00e54f0bd258ab25e7f731ca1d5144b0bf7bec0051abccd2bdcff65fa3262c9"}, + {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f8e845d894e39fb53834da826078f6dc1a933b32b1478cf437007367efaf6f6a"}, + {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:840456cb1067dc350af9080298c7c2cfdddcedc1cb1e0b30dceecdaf7be1a2d3"}, + {file = "coverage-7.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c11ca2df2206a4e3e4c4567f52594637392ed05d7c7fb73b4ea1c658ba560265"}, + {file = "coverage-7.4.2-cp39-cp39-win32.whl", hash = "sha256:3ff5bdb08d8938d336ce4088ca1a1e4b6c8cd3bef8bb3a4c0eb2f37406e49643"}, + {file = "coverage-7.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:ac9e95cefcf044c98d4e2c829cd0669918585755dd9a92e28a1a7012322d0a95"}, + {file = "coverage-7.4.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:f593a4a90118d99014517c2679e04a4ef5aee2d81aa05c26c734d271065efcb6"}, + {file = "coverage-7.4.2.tar.gz", hash = "sha256:1a5ee18e3a8d766075ce9314ed1cb695414bae67df6a4b0805f5137d93d6f1cb"}, ] [package.dependencies] @@ -415,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 = [ @@ -426,6 +444,7 @@ files = [ name = "docutils" version = "0.20.1" description = "Docutils -- Python Documentation Utilities" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -437,6 +456,7 @@ files = [ name = "eradicate" version = "2.3.0" description = "Removes commented-out code." +category = "dev" optional = false python-versions = "*" files = [ @@ -446,13 +466,14 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -462,6 +483,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 = [ @@ -483,6 +505,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 = [ @@ -499,6 +522,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 = [ @@ -515,6 +539,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 = [ @@ -529,6 +554,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 = [ @@ -547,6 +573,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 = [ @@ -561,6 +588,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 = [ @@ -575,6 +603,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 = [ @@ -590,6 +619,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 = [ @@ -605,6 +635,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 = [ @@ -621,6 +652,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 = [ @@ -639,6 +671,7 @@ test = ["pytest-cov"] name = "flake8-polyfill" version = "1.0.2" description = "Polyfill package for Flake8 plugins" +category = "dev" optional = false python-versions = "*" files = [ @@ -651,21 +684,24 @@ flake8 = "*" [[package]] name = "flake8-quotes" -version = "3.3.2" +version = "3.4.0" description = "Flake8 lint for quotes." +category = "dev" optional = false python-versions = "*" files = [ - {file = "flake8-quotes-3.3.2.tar.gz", hash = "sha256:6e26892b632dacba517bf27219c459a8396dcfac0f5e8204904c5a4ba9b480e1"}, + {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 = [ @@ -682,6 +718,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 = [ @@ -696,6 +733,7 @@ flake8 = "*" name = "gitdb" version = "4.0.11" description = "Git Object Database" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -708,25 +746,27 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.40" +version = "3.1.42" 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.40-py3-none-any.whl", hash = "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a"}, - {file = "GitPython-3.1.40.tar.gz", hash = "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4"}, + {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, + {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, ] [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", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-sugar"] +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"] [[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 = [ @@ -736,13 +776,14 @@ files = [ [[package]] name = "httpcore" -version = "1.0.1" +version = "1.0.2" description = "A minimal low-level HTTP client." +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.1-py3-none-any.whl", hash = "sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0"}, - {file = "httpcore-1.0.1.tar.gz", hash = "sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92"}, + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, ] [package.dependencies] @@ -752,48 +793,51 @@ 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.1" +version = "0.25.2" description = "The next generation HTTP client." +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"}, - {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"}, + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, ] [package.dependencies] anyio = "*" certifi = "*" -httpcore = "*" +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.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -805,6 +849,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 = [ @@ -822,6 +867,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 = [ @@ -840,6 +886,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 = [ @@ -851,6 +898,7 @@ files = [ name = "mypy" version = "0.910" description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -892,6 +940,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 = "main" optional = false python-versions = ">=2.7" files = [ @@ -902,6 +951,7 @@ files = [ name = "packaging" version = "23.2" description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -911,19 +961,21 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {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 = [ @@ -935,6 +987,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 = [ @@ -948,28 +1001,30 @@ flake8-polyfill = ">=1.0.2,<2" [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +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)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -980,6 +1035,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 = [ @@ -989,47 +1045,48 @@ files = [ [[package]] name = "pydantic" -version = "1.10.13" +version = "1.10.14" description = "Data validation and settings management using python type hints" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, - {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, - {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, - {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, - {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, - {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, - {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, - {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, - {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, - {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, + {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"}, ] [package.dependencies] @@ -1043,6 +1100,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 = [ @@ -1060,6 +1118,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 = [ @@ -1069,22 +1128,43 @@ files = [ [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [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 = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pytest" version = "7.2.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1108,6 +1188,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 = [ @@ -1125,6 +1206,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 = [ @@ -1143,6 +1225,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 = [ @@ -1202,6 +1285,7 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1223,6 +1307,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 = [ @@ -1237,6 +1322,7 @@ httpx = ">=0.21.0" name = "restructuredtext-lint" version = "1.4.0" description = "reStructuredText linter" +category = "dev" optional = false python-versions = "*" files = [ @@ -1246,10 +1332,28 @@ files = [ [package.dependencies] docutils = ">=0.11,<1.0" +[[package]] +name = "setuptools" +version = "69.1.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"}, + {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"}, +] + +[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)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "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", "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.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "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 = [ @@ -1261,6 +1365,7 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1272,6 +1377,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 = [ @@ -1283,6 +1389,7 @@ files = [ name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1301,6 +1408,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "stevedore" version = "5.1.0" description = "Manage dynamic plugins for Python applications" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1315,6 +1423,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 = [ @@ -1326,6 +1435,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 = [ @@ -1337,6 +1447,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1346,29 +1457,31 @@ files = [ [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] name = "urllib3" -version = "2.0.7" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1376,6 +1489,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 = [ @@ -1395,6 +1509,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 = [ @@ -1427,6 +1542,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 = [ @@ -1440,4 +1556,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "0043d5e2f76626f6c37b292c36d8d0e8f5b91bb4fa25da686519e7b432307320" +content-hash = "4ff9c20ba079b94ab21dee10e4abbc1dce06034fcdd89784511e6d8423096aa0" diff --git a/pybotx/__init__.py b/pybotx/__init__.py index 7df455c8..427b8ec6 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -3,17 +3,25 @@ UnsupportedBotAPIVersionError, ) from pybotx.bot.api.responses.bot_disabled import ( + BotAPIBotDisabledErrorData, BotAPIBotDisabledResponse, build_bot_disabled_response, ) from pybotx.bot.api.responses.command_accepted import build_command_accepted_response +from pybotx.bot.api.responses.unverified_request import ( + BotAPIUnverifiedRequestErrorData, + BotAPIUnverifiedRequestResponse, + build_unverified_request_response, +) from pybotx.bot.bot import Bot from pybotx.bot.callbacks.callback_repo_proto import CallbackRepoProto from pybotx.bot.exceptions import ( AnswerDestinationLookupError, BotShuttingDownError, BotXMethodCallbackNotFoundError, + RequestHeadersNotProvidedError, UnknownBotAccountError, + UnverifiedRequestError, ) from pybotx.bot.handler import IncomingMessageHandlerFunc, Middleware from pybotx.bot.handler_collector import HandlerCollector @@ -121,14 +129,17 @@ __all__ = ( "AddedToChatEvent", "AnswerDestinationLookupError", - "AttachmentTypes", "AttachmentDocument", "AttachmentImage", - "AttachmentVoice", + "AttachmentTypes", "AttachmentVideo", + "AttachmentVoice", "Bot", + "BotAPIBotDisabledErrorData", "BotAPIBotDisabledResponse", "BotAPIMethodFailedCallback", + "BotAPIUnverifiedRequestErrorData", + "BotAPIUnverifiedRequestResponse", "BotAccount", "BotAccountWithSecret", "BotIsNotChatMemberError", @@ -137,13 +148,13 @@ "BotShuttingDownError", "BotXMethodCallbackNotFoundError", "BotXMethodFailedCallbackReceivedError", + "BotsListItem", "BubbleMarkup", "Button", "ButtonRow", "ButtonTextAlign", "CTSLoginEvent", "CTSLogoutEvent", - "EventEdit", "CallbackNotReceivedError", "CallbackRepoProto", "CantUpdatePersonalChatError", @@ -161,6 +172,7 @@ "DeletedFromChatEvent", "Document", "EditMessage", + "EventEdit", "EventNotFoundError", "File", "FileDeletedError", @@ -198,11 +210,11 @@ "RateLimitReachedError", "Reply", "ReplyMessage", + "RequestHeadersNotProvidedError", "SmartApp", "SmartAppEvent", "SmartAppEvent", "StatusRecipient", - "BotsListItem", "StealthModeDisabledError", "Sticker", "StickerPack", @@ -211,6 +223,7 @@ "UnknownBotAccountError", "UnknownSystemEventError", "UnsupportedBotAPIVersionError", + "UnverifiedRequestError", "UserDevice", "UserFromCSV", "UserFromSearch", @@ -221,6 +234,7 @@ "Voice", "build_bot_disabled_response", "build_command_accepted_response", + "build_unverified_request_response", "lifespan_wrapper", ) diff --git a/pybotx/bot/api/responses/bot_disabled.py b/pybotx/bot/api/responses/bot_disabled.py index 6843728f..f550c22f 100644 --- a/pybotx/bot/api/responses/bot_disabled.py +++ b/pybotx/bot/api/responses/bot_disabled.py @@ -23,7 +23,7 @@ class BotAPIBotDisabledResponse: def build_bot_disabled_response(status_message: str) -> Dict[str, Any]: """Build bot disabled response for BotX. - It should be send if the bot can't process the command. + It should be sent if the bot can't process the command. If you would like to build complex response, see `BotAPIBotDisabledResponse`. :param status_message: Status message. diff --git a/pybotx/bot/api/responses/unverified_request.py b/pybotx/bot/api/responses/unverified_request.py new file mode 100644 index 00000000..23ba8efb --- /dev/null +++ b/pybotx/bot/api/responses/unverified_request.py @@ -0,0 +1,39 @@ +from dataclasses import asdict, dataclass, field +from typing import Any, Dict, List, Literal + + +@dataclass +class BotAPIUnverifiedRequestErrorData: + status_message: str + + +@dataclass +class BotAPIUnverifiedRequestResponse: + """`Unverified request` response model. + + Only `.error_data.status_message` attribute will be displayed to + user. Other attributes will be visible only in BotX logs. + """ + + error_data: BotAPIUnverifiedRequestErrorData + errors: List[str] = field(default_factory=list) + reason: Literal["unverified_request"] = "unverified_request" + + +def build_unverified_request_response(status_message: str) -> Dict[str, Any]: + """Build `unverified request` response for BotX. + + It should be sent if the header with the authorization token is missing or + the authorization token is invalid. + If you would like to build complex response, see `BotAPIUnverifiedRequestResponse`. + + :param status_message: Status message. + + :return: Built `unverified request` response. + """ + + response = BotAPIUnverifiedRequestResponse( + error_data=BotAPIUnverifiedRequestErrorData(status_message=status_message), + ) + + return asdict(response) diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index c850cdb8..7705121d 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -9,6 +9,7 @@ Dict, Iterator, List, + Mapping, Optional, Sequence, Tuple, @@ -18,6 +19,7 @@ import aiofiles import httpx +import jwt from aiocsv.readers import AsyncDictReader from aiofiles.tempfile import NamedTemporaryFile, TemporaryDirectory from pydantic import ValidationError, parse_obj_as @@ -28,7 +30,12 @@ from pybotx.bot.callbacks.callback_memory_repo import CallbackMemoryRepo from pybotx.bot.callbacks.callback_repo_proto import CallbackRepoProto from pybotx.bot.contextvars import bot_id_var, chat_id_var -from pybotx.bot.exceptions import AnswerDestinationLookupError +from pybotx.bot.exceptions import ( + AnswerDestinationLookupError, + RequestHeadersNotProvidedError, + UnknownBotAccountError, + UnverifiedRequestError, +) from pybotx.bot.handler import Middleware from pybotx.bot.handler_collector import HandlerCollector from pybotx.bot.middlewares.exception_middleware import ExceptionHandlersDict @@ -274,6 +281,8 @@ def __init__( def async_execute_raw_bot_command( self, raw_bot_command: Dict[str, Any], + verify_request: bool = True, + request_headers: Optional[Mapping[str, str]] = None, logging_command: bool = True, ) -> None: if logging_command: @@ -284,6 +293,11 @@ def async_execute_raw_bot_command( ), ) + if verify_request: + if request_headers is None: + raise RequestHeadersNotProvidedError + self._verify_request(request_headers) + try: bot_api_command: BotAPICommand = parse_obj_as( # Same ignore as in pydantic @@ -305,12 +319,22 @@ def async_execute_bot_command( return self._handler_collector.async_handle_bot_command(self, bot_command) - async def raw_get_status(self, query_params: Dict[str, str]) -> Dict[str, Any]: + async def raw_get_status( + self, + query_params: Dict[str, str], + verify_request: bool = True, + request_headers: Optional[Mapping[str, str]] = None, + ) -> Dict[str, Any]: logger.opt(lazy=True).debug( "Got status: {status}", status=lambda: pformat_jsonable_obj(query_params), ) + if verify_request: + if request_headers is None: + raise RequestHeadersNotProvidedError + self._verify_request(request_headers) + try: bot_api_status_recipient = BotAPIStatusRecipient.parse_obj(query_params) except ValidationError as exc: @@ -330,9 +354,16 @@ async def get_status(self, status_recipient: StatusRecipient) -> BotMenu: async def set_raw_botx_method_result( self, raw_botx_method_result: Dict[str, Any], + verify_request: bool = True, + request_headers: Optional[Mapping[str, str]] = None, ) -> None: logger.debug("Got callback: {callback}", callback=raw_botx_method_result) + if verify_request: + if request_headers is None: + raise RequestHeadersNotProvidedError + self._verify_request(request_headers) + callback: BotXMethodCallback = parse_obj_as( # Same ignore as in pydantic BotXMethodCallback, # type: ignore[arg-type] @@ -1909,6 +1940,48 @@ async def collect_metric( ) await method.execute(payload) + def _verify_request(self, headers: Mapping[str, str]) -> None: # noqa: WPS238 + authorization_header = headers.get("authorization") + if not authorization_header: + raise UnverifiedRequestError("The authorization token was not provided.") + + token = authorization_header.split()[-1] + decode_algorithms = ["HS256"] + + try: + token_payload = jwt.decode( + jwt=token, + algorithms=decode_algorithms, + options={ + "verify_signature": False, + }, + ) + except jwt.DecodeError as decode_exc: + raise UnverifiedRequestError(decode_exc.args[0]) from decode_exc + + audience = token_payload.get("aud") + if not audience or not isinstance(audience, Sequence) or len(audience) != 1: + raise UnverifiedRequestError("Invalid audience parameter was provided.") + + try: + bot_account = self._bot_accounts_storage.get_bot_account(UUID(audience[-1])) + except UnknownBotAccountError as unknown_bot_exc: + raise UnverifiedRequestError(unknown_bot_exc.args[0]) from unknown_bot_exc + + try: + jwt.decode( + jwt=token, + key=bot_account.secret_key, + algorithms=decode_algorithms, + issuer=bot_account.host, + leeway=0.5, + options={ + "verify_aud": False, + }, + ) + except jwt.InvalidTokenError as exc: + raise UnverifiedRequestError(exc.args[0]) from exc + @staticmethod def _build_main_collector( collectors: Sequence[HandlerCollector], diff --git a/pybotx/bot/bot_accounts_storage.py b/pybotx/bot/bot_accounts_storage.py index f9ca95ea..1ae97d6b 100644 --- a/pybotx/bot/bot_accounts_storage.py +++ b/pybotx/bot/bot_accounts_storage.py @@ -13,11 +13,18 @@ def __init__(self, bot_accounts: List[BotAccountWithSecret]) -> None: self._bot_accounts = bot_accounts self._auth_tokens: Dict[UUID, str] = {} + def get_bot_account(self, bot_id: UUID) -> BotAccountWithSecret: + for bot_account in self._bot_accounts: + if bot_account.id == bot_id: + return bot_account + + raise UnknownBotAccountError(bot_id) + def iter_bot_accounts(self) -> Iterator[BotAccount]: yield from self._bot_accounts def get_host(self, bot_id: UUID) -> str: - bot_account = self._get_bot_account(bot_id) + bot_account = self.get_bot_account(bot_id) return bot_account.host def set_token(self, bot_id: UUID, token: str) -> None: @@ -27,7 +34,7 @@ def get_token_or_none(self, bot_id: UUID) -> Optional[str]: return self._auth_tokens.get(bot_id) def build_signature(self, bot_id: UUID) -> str: - bot_account = self._get_bot_account(bot_id) + bot_account = self.get_bot_account(bot_id) signed_bot_id = hmac.new( key=bot_account.secret_key.encode(), @@ -38,11 +45,4 @@ def build_signature(self, bot_id: UUID) -> str: return base64.b16encode(signed_bot_id).decode() def ensure_bot_id_exists(self, bot_id: UUID) -> None: - self._get_bot_account(bot_id) - - def _get_bot_account(self, bot_id: UUID) -> BotAccountWithSecret: - for bot_account in self._bot_accounts: - if bot_account.id == bot_id: - return bot_account - - raise UnknownBotAccountError(bot_id) + self.get_bot_account(bot_id) diff --git a/pybotx/bot/exceptions.py b/pybotx/bot/exceptions.py index 16bc7012..0257106d 100644 --- a/pybotx/bot/exceptions.py +++ b/pybotx/bot/exceptions.py @@ -29,3 +29,14 @@ class AnswerDestinationLookupError(Exception): def __init__(self) -> None: self.message = "No IncomingMessage received. Use `Bot.send` instead" super().__init__(self.message) + + +class RequestHeadersNotProvidedError(Exception): + def __init__(self, *args: Any) -> None: + reason = "To verify the request you should provide headers." + message = args[0] if args else reason + super().__init__(message) + + +class UnverifiedRequestError(Exception): + """The authorization header is missing or the token is invalid.""" diff --git a/pyproject.toml b/pyproject.toml index dd1b0e55..8f9f7f06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.62.1" +version = "0.63.0" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", @@ -18,11 +18,16 @@ python = ">=3.8,<3.12" aiofiles = ">=0.7.0,<0.9.0" httpx = "^0.25.0" +# The v1.0.3 cause some troubles with no-wait callbacks functionality. +# It will be fixed in the next versions. +# 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" [tool.poetry.dev-dependencies] add-trailing-comma = "2.2.1" diff --git a/setup.cfg b/setup.cfg index 575e5756..25f477c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,7 +70,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,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 # Import ignores for README lint .snippets/*:F403,F405,WPS347,WPS421,S106,WPS237 diff --git a/tests/client/notifications_api/test_direct_notification.py b/tests/client/notifications_api/test_direct_notification.py index 02f393b6..f9003c10 100644 --- a/tests/client/notifications_api/test_direct_notification.py +++ b/tests/client/notifications_api/test_direct_notification.py @@ -149,7 +149,7 @@ async def hello_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) await asyncio.sleep(0) # Return control to event loop @@ -159,6 +159,7 @@ async def hello_handler(message: IncomingMessage, bot: Bot) -> None: "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -275,7 +276,7 @@ async def hello_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) await asyncio.sleep(0) # Return control to event loop @@ -285,6 +286,7 @@ async def hello_handler(message: IncomingMessage, bot: Bot) -> None: "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -367,6 +369,7 @@ async def test__send_message__chat_not_found_error_raised( "error_description": "Chat with specified id not found", }, }, + verify_request=False, ) # - Assert - @@ -427,6 +430,7 @@ async def test__send_message__bot_is_not_a_chat_member_error_raised( "error_description": "Bot is not a chat member", }, }, + verify_request=False, ) # - Assert - @@ -488,6 +492,7 @@ async def test__send_message__event_recipients_list_is_empty_error_raised( "error_description": "Event recipients list is empty", }, }, + verify_request=False, ) # - Assert - @@ -548,6 +553,7 @@ async def test__send_message__stealth_mode_disabled_error_raised( "error_description": "Stealth mode disabled in specified chat", }, }, + verify_request=False, ) # - Assert - @@ -602,6 +608,7 @@ async def test__send_message__miminally_filled_succeed( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -762,6 +769,7 @@ async def test__send_message__maximum_filled_succeed( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -885,6 +893,7 @@ async def test__send_message__all_mentions_types_succeed( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -969,6 +978,7 @@ async def test__send_message__message_body_max_length_succeed( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - diff --git a/tests/client/notifications_api/test_internal_bot_notification.py b/tests/client/notifications_api/test_internal_bot_notification.py index 4ab5166e..b6f4acc0 100644 --- a/tests/client/notifications_api/test_internal_bot_notification.py +++ b/tests/client/notifications_api/test_internal_bot_notification.py @@ -118,6 +118,7 @@ async def test__send_internal_bot_notification__chat_not_found_error_raised( ), }, }, + verify_request=False, ) with pytest.raises(ChatNotFoundError) as exc: @@ -177,6 +178,7 @@ async def test__send_internal_bot_notification__bot_is_not_chat_member_error_rai "error_description": "Bot is not a chat member", }, }, + verify_request=False, ) with pytest.raises(BotIsNotChatMemberError) as exc: @@ -237,6 +239,7 @@ async def test__send_internal_bot_notification__final_recipients_list_empty_erro "error_description": "Event recipients list is empty", }, }, + verify_request=False, ) with pytest.raises(FinalRecipientsListEmptyError) as exc: @@ -290,6 +293,7 @@ async def test__send_internal_bot_notification__succeed( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - diff --git a/tests/client/notifications_api/test_markup.py b/tests/client/notifications_api/test_markup.py index 3b80e130..25ea974e 100644 --- a/tests/client/notifications_api/test_markup.py +++ b/tests/client/notifications_api/test_markup.py @@ -105,6 +105,7 @@ async def test__markup__defaults_filled( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -225,6 +226,7 @@ async def test__markup__correctly_built( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -364,6 +366,7 @@ async def test__markup__color_and_align( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - diff --git a/tests/client/smartapps_api/test_smartapp_custom_notification.py b/tests/client/smartapps_api/test_smartapp_custom_notification.py index 257648ef..441b2c34 100644 --- a/tests/client/smartapps_api/test_smartapp_custom_notification.py +++ b/tests/client/smartapps_api/test_smartapp_custom_notification.py @@ -64,6 +64,7 @@ async def test__send_smartapp_custom_notification__succeed( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - diff --git a/tests/client/test_botx_method_callback.py b/tests/client/test_botx_method_callback.py index f0c2b7ce..3dadb695 100644 --- a/tests/client/test_botx_method_callback.py +++ b/tests/client/test_botx_method_callback.py @@ -128,6 +128,7 @@ async def test__botx_method_callback__callback_not_found( ), }, }, + verify_request=False, ) # - Assert - @@ -180,6 +181,7 @@ async def test__botx_method_callback__error_callback_error_handler_called( ), }, }, + verify_request=False, ) with pytest.raises(FooBarError) as exc: @@ -235,6 +237,7 @@ async def test__botx_method_callback__error_callback_received( ), }, }, + verify_request=False, ) with pytest.raises(BotXMethodFailedCallbackReceivedError) as exc: @@ -323,6 +326,7 @@ async def test__botx_method_callback__callback_received_after_timeout( ), }, }, + verify_request=False, ) # - Assert - @@ -440,6 +444,7 @@ async def test__botx_method_callback__callback_successful_received( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -488,6 +493,7 @@ async def test__botx_method_callback__callback_successful_received_with_custom_r "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) # - Assert - @@ -538,6 +544,7 @@ async def test__botx_method_callback__bot_wait_callback_before_its_receiving( "status": "ok", "result": {}, }, + verify_request=False, ) callback = await task @@ -590,6 +597,7 @@ async def test__botx_method_callback__bot_wait_callback_after_its_receiving( "status": "ok", "result": {}, }, + verify_request=False, ) callback = await bot.wait_botx_method_callback(foo_bar) @@ -637,7 +645,7 @@ async def test__botx_method_callback__bot_dont_wait_received_callback( async with lifespan_wrapper(built_bot) as bot: await bot.call_foo_bar(bot_id, baz=1, callback_timeout=0, wait_callback=False) - # Return control to event loop + # Return control to event-loop await asyncio.sleep(0) await bot.set_raw_botx_method_result( @@ -646,6 +654,7 @@ async def test__botx_method_callback__bot_dont_wait_received_callback( "status": "ok", "result": {}, }, + verify_request=False, ) # - Assert - @@ -693,6 +702,7 @@ async def test__botx_method_callback__bot_wait_already_waited_callback( "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", "result": {}, }, + verify_request=False, ) foo_bar = await task diff --git a/tests/conftest.py b/tests/conftest.py index 489db883..35611745 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from uuid import UUID, uuid4 import httpx +import jwt import pytest from aiofiles.tempfile import NamedTemporaryFile from pydantic import BaseModel @@ -70,6 +71,30 @@ def bot_account(host: str, bot_id: UUID) -> BotAccountWithSecret: ) +@pytest.fixture +def authorization_token_payload(bot_account: BotAccountWithSecret) -> Dict[str, Any]: + return { + "aud": [str(bot_account.id)], + "exp": datetime(year=3000, month=1, day=1).timestamp(), + "iat": datetime(year=2000, month=1, day=1).timestamp(), + "iss": bot_account.host, + "jti": "2uqpju31h6dgv4f41c005e1i", + "nbf": datetime(year=2000, month=1, day=1).timestamp(), + } + + +@pytest.fixture +def authorization_header( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> Dict[str, str]: + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + return {"authorization": f"Bearer {token}"} + + @pytest.fixture def bot_signature() -> str: return "E050AEEA197E0EF0A6E1653E18B7D41C7FDEC0FCFBA44C44FCCD2A88CEABD130" diff --git a/tests/system_events/test_added_to_chat.py b/tests/system_events/test_added_to_chat.py index d0178c57..c26a0b82 100644 --- a/tests/system_events/test_added_to_chat.py +++ b/tests/system_events/test_added_to_chat.py @@ -80,7 +80,7 @@ async def added_to_chat_handler(event: AddedToChatEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert added_to_chat == AddedToChatEvent( diff --git a/tests/system_events/test_chat_created.py b/tests/system_events/test_chat_created.py index fd5db45f..dbc9bef5 100644 --- a/tests/system_events/test_chat_created.py +++ b/tests/system_events/test_chat_created.py @@ -96,7 +96,7 @@ async def chat_created_handler(event: ChatCreatedEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert chat_created == ChatCreatedEvent( diff --git a/tests/system_events/test_cts_login.py b/tests/system_events/test_cts_login.py index 797b6667..795daf4b 100644 --- a/tests/system_events/test_cts_login.py +++ b/tests/system_events/test_cts_login.py @@ -76,7 +76,7 @@ async def cts_login_handler(event: CTSLoginEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert cts_login == CTSLoginEvent( diff --git a/tests/system_events/test_cts_logout.py b/tests/system_events/test_cts_logout.py index d8c3351a..dd95c878 100644 --- a/tests/system_events/test_cts_logout.py +++ b/tests/system_events/test_cts_logout.py @@ -76,7 +76,7 @@ async def cts_logout_handler(event: CTSLogoutEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert cts_logout == CTSLogoutEvent( diff --git a/tests/system_events/test_deleted_from_chat.py b/tests/system_events/test_deleted_from_chat.py index 82b1a561..c429fdb1 100644 --- a/tests/system_events/test_deleted_from_chat.py +++ b/tests/system_events/test_deleted_from_chat.py @@ -79,7 +79,7 @@ async def deleted_from_chat_handler(event: DeletedFromChatEvent, bot: Bot) -> No # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert deleted_from_chat == DeletedFromChatEvent( diff --git a/tests/system_events/test_event_edit.py b/tests/system_events/test_event_edit.py index 66924c5d..6a07b1a6 100644 --- a/tests/system_events/test_event_edit.py +++ b/tests/system_events/test_event_edit.py @@ -97,7 +97,7 @@ async def event_edit_handler(event: EventEdit, _: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert event_edit == EventEdit( diff --git a/tests/system_events/test_internal_bot_notification.py b/tests/system_events/test_internal_bot_notification.py index 40f94f64..ea1bc6d0 100644 --- a/tests/system_events/test_internal_bot_notification.py +++ b/tests/system_events/test_internal_bot_notification.py @@ -85,7 +85,7 @@ async def internal_bot_notification_handler( # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert internal_bot_notification == InternalBotNotificationEvent( diff --git a/tests/system_events/test_left_from_chat.py b/tests/system_events/test_left_from_chat.py index d757b7a4..f9031e1c 100644 --- a/tests/system_events/test_left_from_chat.py +++ b/tests/system_events/test_left_from_chat.py @@ -79,7 +79,7 @@ async def left_from_chat_handler(event: LeftFromChatEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert left_from_chat == LeftFromChatEvent( diff --git a/tests/system_events/test_smartapp_event.py b/tests/system_events/test_smartapp_event.py index b39d141d..76057309 100644 --- a/tests/system_events/test_smartapp_event.py +++ b/tests/system_events/test_smartapp_event.py @@ -105,7 +105,7 @@ async def smartapp_handler(event: SmartAppEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert smartapp == SmartAppEvent( diff --git a/tests/test_attachments.py b/tests/test_attachments.py index cf56dfce..a6bab9f2 100644 --- a/tests/test_attachments.py +++ b/tests/test_attachments.py @@ -65,7 +65,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) await asyncio.sleep(0) # Return control to event loop @@ -176,7 +176,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert getattr(incoming_message, attr_name) == domain_attachment @@ -297,7 +297,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert incoming_message @@ -319,7 +319,7 @@ async def test__async_execute_raw_bot_command__unknown_attachment_type( # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert "Received unknown attachment type" in loguru_caplog.text @@ -354,7 +354,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert incoming_message diff --git a/tests/test_base_command.py b/tests/test_base_command.py index 36e21db6..3a8d2600 100644 --- a/tests/test_base_command.py +++ b/tests/test_base_command.py @@ -28,7 +28,7 @@ async def test__async_execute_raw_bot_command__invalid_payload_value_error_raise # - Act - async with lifespan_wrapper(built_bot) as bot: with pytest.raises(ValueError) as exc: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert "validation" in str(exc.value) @@ -42,7 +42,7 @@ async def test__async_execute_raw_bot_command__unsupported_bot_api_version_error # - Act - async with lifespan_wrapper(built_bot) as bot: with pytest.raises(UnsupportedBotAPIVersionError) as exc: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert "Unsupported" in str(exc.value) @@ -90,7 +90,7 @@ async def test__async_execute_raw_bot_command__unknown_system_event() -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: with pytest.raises(UnknownSystemEventError) as exc: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert "Unknown system event" in str(exc.value) @@ -113,7 +113,7 @@ async def test__async_execute_raw_bot_command__logging_incoming_request( # - Act - async with lifespan_wrapper(built_bot) as bot: with loguru_caplog.at_level(logging.DEBUG): - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert log_message in loguru_caplog.messages @@ -135,7 +135,11 @@ async def test__async_execute_raw_bot_command__not_logging_incoming_request( # - Act - async with lifespan_wrapper(built_bot) as bot: with loguru_caplog.at_level(logging.DEBUG): - bot.async_execute_raw_bot_command(payload, logging_command=False) + bot.async_execute_raw_bot_command( + payload, + verify_request=False, + logging_command=False, + ) # - Assert - assert log_message not in loguru_caplog.messages diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index fb01b688..dc1524f0 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -16,9 +16,13 @@ HandlerCollector, IncomingMessage, UnknownBotAccountError, + UnverifiedRequestError, build_bot_disabled_response, build_command_accepted_response, ) +from pybotx.bot.api.responses.unverified_request import ( + build_unverified_request_response, +) # - Bot setup - collector = HandlerCollector() @@ -53,7 +57,7 @@ async def command_handler( bot: Bot = bot_dependency, ) -> JSONResponse: try: - bot.async_execute_raw_bot_command(await request.json()) + bot.async_execute_raw_bot_command(await request.json(), verify_request=False) except ValueError: error_label = "Bot command validation error" logger.exception(error_label) @@ -79,7 +83,27 @@ async def command_handler( @router.get("/status") async def status_handler(request: Request, bot: Bot = bot_dependency) -> JSONResponse: - status = await bot.raw_get_status(dict(request.query_params)) + status = await bot.raw_get_status(dict(request.query_params), verify_request=False) + return JSONResponse(status) + + +@router.get("/status__unverified_request") +async def status_handler__unverified_request( + request: Request, + bot: Bot = bot_dependency, +) -> JSONResponse: + try: + status = await bot.raw_get_status( + dict(request.query_params), + request_headers=request.headers, + ) + except UnverifiedRequestError as exc: + return JSONResponse( + content=build_unverified_request_response( + status_message=exc.args[0], + ), + status_code=HTTPStatus.UNAUTHORIZED, + ) return JSONResponse(status) @@ -88,7 +112,7 @@ async def callback_handler( request: Request, bot: Bot = bot_dependency, ) -> JSONResponse: - await bot.set_raw_botx_method_result(await request.json()) + await bot.set_raw_botx_method_result(await request.json(), verify_request=False) return JSONResponse( build_command_accepted_response(), status_code=HTTPStatus.ACCEPTED, @@ -324,3 +348,22 @@ def test__web_app__disabled_bot_response( "errors": [], "reason": "bot_disabled", } + + +def test__web_app__unverified_request_response( + bot: Bot, +) -> None: + # - Act - + with TestClient(fastapi_factory(bot)) as test_client: + response = test_client.get( + "/status__unverified_request", + params={}, + ) + + # - Assert - + assert response.status_code == HTTPStatus.UNAUTHORIZED + assert response.json() == { + "error_data": {"status_message": "The authorization token was not provided."}, + "errors": [], + "reason": "unverified_request", + } diff --git a/tests/test_files.py b/tests/test_files.py index 5b13411c..5fac5c20 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -126,7 +126,7 @@ async def smartapp_event_handler(event: SmartAppEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert read_content == b"Hello, world!\n" @@ -317,7 +317,7 @@ async def smartapp_event_handler(event: SmartAppEvent, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert smartapp_event diff --git a/tests/test_incoming_message.py b/tests/test_incoming_message.py index 0886fba6..b388e036 100644 --- a/tests/test_incoming_message.py +++ b/tests/test_incoming_message.py @@ -85,7 +85,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert incoming_message == IncomingMessage( @@ -243,7 +243,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert incoming_message == IncomingMessage( @@ -431,7 +431,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert incoming_message @@ -515,7 +515,7 @@ async def test__async_execute_raw_bot_command__unknown_entity_type( # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert "Received unknown entity type" in loguru_caplog.text @@ -575,7 +575,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert incoming_message diff --git a/tests/test_logs.py b/tests/test_logs.py index ac1460e2..157ae8da 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -55,7 +55,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert "..." in loguru_caplog.text diff --git a/tests/test_status.py b/tests/test_status.py index f662204d..87bbbeae 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -151,7 +151,7 @@ async def handler(message: IncomingMessage, bot: Bot) -> None: # - Act - with pytest.raises(ValueError) as exc: async with lifespan_wrapper(built_bot) as bot: - await bot.raw_get_status(query) + await bot.raw_get_status(query, verify_request=False) # - Assert - assert "validation error" in str(exc.value) @@ -170,7 +170,7 @@ async def test__raw_get_status__unknown_bot_account_error_raised() -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: with pytest.raises(UnknownBotAccountError) as exc: - await bot.raw_get_status(query) + await bot.raw_get_status(query, verify_request=False) # - Assert - assert "123e4567-e89b-12d3-a456-426655440000" in str(exc.value) @@ -191,7 +191,7 @@ async def test__raw_get_status__minimally_filled_succeed( # - Act - async with lifespan_wrapper(built_bot) as bot: - status = await bot.raw_get_status(query) + status = await bot.raw_get_status(query, verify_request=False) # - Assert - assert status @@ -215,7 +215,7 @@ async def test__raw_get_status__minimum_filled_succeed( # - Act - async with lifespan_wrapper(built_bot) as bot: - status = await bot.raw_get_status(query) + status = await bot.raw_get_status(query, verify_request=False) # - Assert - assert status @@ -239,7 +239,7 @@ async def test__raw_get_status__maximum_filled_succeed( # - Act - async with lifespan_wrapper(built_bot) as bot: - status = await bot.raw_get_status(query) + status = await bot.raw_get_status(query, verify_request=False) # - Assert - assert status @@ -266,7 +266,7 @@ async def handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - status = await bot.raw_get_status(query) + status = await bot.raw_get_status(query, verify_request=False) # - Assert - assert status == { @@ -300,7 +300,7 @@ async def handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - status = await bot.raw_get_status(query) + status = await bot.raw_get_status(query, verify_request=False) # - Assert - assert status == { @@ -337,7 +337,7 @@ async def test__get_status__unsupported_chat_type_accepted( # - Act - async with lifespan_wrapper(built_bot) as bot: - status = await bot.raw_get_status(query) + status = await bot.raw_get_status(query, verify_request=False) # - Assert - assert status diff --git a/tests/test_stickers.py b/tests/test_stickers.py index e4e8e5c5..cf682368 100644 --- a/tests/test_stickers.py +++ b/tests/test_stickers.py @@ -71,7 +71,7 @@ async def default_handler(message: IncomingMessage, bot: Bot) -> None: # - Act - async with lifespan_wrapper(built_bot) as bot: - bot.async_execute_raw_bot_command(payload) + bot.async_execute_raw_bot_command(payload, verify_request=False) # - Assert - assert await async_buffer.read() == PNG_IMAGE diff --git a/tests/test_verify_request.py b/tests/test_verify_request.py new file mode 100644 index 00000000..d2082271 --- /dev/null +++ b/tests/test_verify_request.py @@ -0,0 +1,437 @@ +from datetime import datetime +from typing import Any, Callable, Coroutine, Dict +from unittest.mock import AsyncMock, Mock +from uuid import uuid4 + +import jwt +import pytest + +from pybotx import ( + Bot, + BotAccountWithSecret, + BotMenu, + HandlerCollector, + RequestHeadersNotProvidedError, + UnverifiedRequestError, + lifespan_wrapper, +) + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + + +async def test__verify_request__success_attempt( + bot_account: BotAccountWithSecret, + authorization_header: Dict[str, str], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act and Assert - + async with lifespan_wrapper(built_bot) as bot: + bot._verify_request(authorization_header) + + +async def test__verify_request__no_authorization_header_provided( + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({}) + + # - Assert - + assert "The authorization token was not provided." in str(exc.value) + + +async def test__verify_request__cannot_decode_token( + bot_account: BotAccountWithSecret, +) -> 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(UnverifiedRequestError): + bot._verify_request({"authorization": "test"}) + + +async def test__verify_request__aud_is_not_provided( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + authorization_token_payload.pop("aud") + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "Invalid audience parameter was provided." in str(exc.value) + + +async def test__verify_request__aud_is_not_sequence( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + authorization_token_payload["aud"] = 12345 + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "Invalid audience parameter was provided." in str(exc.value) + + +async def test__verify_request__too_many_aud_values( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + authorization_token_payload["aud"] = [str(bot_account.id), str(uuid4())] + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "Invalid audience parameter was provided." in str(exc.value) + + +async def test__verify_request__unknown_aud_value( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + random_bot_id = uuid4() + authorization_token_payload["aud"] = [str(random_bot_id)] + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert f"No bot account with bot_id: `{random_bot_id!s}`" in str(exc.value) + + +async def test__verify_request__invalid_token_secret( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + token = jwt.encode( + payload=authorization_token_payload, + key=str(uuid4()), + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "Signature verification failed" in str(exc.value) + + +async def test__verify_request__expired_signature( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + authorization_token_payload["exp"] = datetime(year=2000, month=1, day=1).timestamp() + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "Signature has expired" in str(exc.value) + + +async def test__verify_request__token_is_not_yet_valid_by_nbf( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + authorization_token_payload["nbf"] = datetime(year=3000, month=1, day=1).timestamp() + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "The token is not yet valid (nbf)" in str(exc.value) + + +async def test__verify_request__token_is_not_yet_valid_by_iat( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + authorization_token_payload["iat"] = datetime(year=3000, month=1, day=1).timestamp() + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "The token is not yet valid (iat)" in str(exc.value) + + +async def test__verify_request__invalid_issuer( + bot_account: BotAccountWithSecret, + authorization_token_payload: Dict[str, Any], +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + authorization_token_payload["iss"] = "another.example.com" + token = jwt.encode( + payload=authorization_token_payload, + key=bot_account.secret_key, + ) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(UnverifiedRequestError) as exc: + bot._verify_request({"authorization": f"Bearer {token}"}) + + # - Assert - + assert "Invalid issuer" in str(exc.value) + + +@pytest.mark.parametrize( + "target_func_name", + ("async_execute_raw_bot_command", "raw_get_status", "set_raw_botx_method_result"), +) +async def test__verify_request__without_headers( + api_incoming_message_factory: Callable[..., Dict[str, Any]], + bot_account: BotAccountWithSecret, + target_func_name: str, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + payload = api_incoming_message_factory() + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(RequestHeadersNotProvidedError) as exc: + target_func = getattr(bot, target_func_name) + result = target_func(payload, verify_request=True) + if isinstance(result, Coroutine): + await result + + # - Assert - + assert "To verify the request you should provide headers." in str(exc.value) + + +async def test__async_execute_raw_bot_command__verify_request__called( + api_incoming_message_factory: Callable[..., Dict[str, Any]], + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + payload = api_incoming_message_factory() + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + bot._verify_request = Mock() # type: ignore + bot.async_execute_raw_bot_command( + payload, + verify_request=True, + request_headers={}, + ) + + # - Assert - + bot._verify_request.assert_called() + + +async def test__raw_get_status__verify_request__called( + api_incoming_message_factory: Callable[..., Dict[str, Any]], + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + bot._verify_request = Mock() # type: ignore + await bot.raw_get_status( + { + "bot_id": str(bot_account.id), + "chat_type": "chat", + "user_huid": "f16cdc5f-6366-5552-9ecd-c36290ab3d11", + }, + verify_request=True, + request_headers={}, + ) + + # - Assert - + bot._verify_request.assert_called() + + +async def test__set_raw_botx_method_result__verify_request__called( + api_incoming_message_factory: Callable[..., Dict[str, Any]], + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + bot._verify_request = Mock() # type: ignore + bot._callbacks_manager.set_botx_method_callback_result = ( # type: ignore + AsyncMock() + ) + await bot.set_raw_botx_method_result( + { + "status": "ok", + "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", + "result": {}, + }, + verify_request=True, + request_headers={}, + ) + + # - Assert - + bot._verify_request.assert_called() + + +async def test__async_execute_raw_bot_command__verify_request__not_called( + api_incoming_message_factory: Callable[..., Dict[str, Any]], + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + payload = api_incoming_message_factory() + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + bot._verify_request = Mock() # type: ignore + bot.async_execute_bot_command = Mock() # type: ignore + bot.async_execute_raw_bot_command(payload, verify_request=False) + + # - Assert - + bot._verify_request.assert_not_called() + bot.async_execute_bot_command.assert_called() + + +async def test__raw_get_status__verify_request__not_called( + api_incoming_message_factory: Callable[..., Dict[str, Any]], + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + bot._verify_request = Mock() # type: ignore + bot.get_status = AsyncMock(return_value=BotMenu({})) # type: ignore + await bot.raw_get_status( + { + "bot_id": "f16cdc5f-6366-5552-9ecd-c36290ab3d11", + "chat_type": "chat", + "user_huid": "f16cdc5f-6366-5552-9ecd-c36290ab3d11", + }, + verify_request=False, + ) + + # - Assert - + bot._verify_request.assert_not_called() + bot.get_status.assert_awaited() + + +async def test__set_raw_botx_method_result__verify_request__not_called( + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + collector = HandlerCollector() + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + bot._verify_request = Mock() # type: ignore + bot._callbacks_manager.set_botx_method_callback_result = ( # type: ignore + AsyncMock() + ) + await bot.set_raw_botx_method_result( + { + "status": "ok", + "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", + "result": {}, + }, + verify_request=False, + ) + + # - Assert - + bot._verify_request.assert_not_called() + bot._callbacks_manager.set_botx_method_callback_result.assert_awaited()