From 25ec5872c6e916d82997d209b9c7054da476029c Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Fri, 11 Jul 2025 22:51:04 +0300 Subject: [PATCH 01/18] feat: bump pydantic --- poetry.lock | 171 ++++++++++++------ pybotx/async_buffer.py | 17 +- pybotx/bot/bot.py | 33 ++-- pybotx/client/botx_method.py | 6 +- pybotx/client/chats_api/chat_info.py | 19 +- pybotx/client/chats_api/list_chats.py | 17 +- pybotx/client/events_api/edit_event.py | 1 + .../client/smartapps_api/smartapp_manifest.py | 3 +- pybotx/client/users_api/user_from_csv.py | 7 +- pybotx/client/users_api/user_from_search.py | 3 +- pybotx/models/api_base.py | 14 +- pybotx/models/async_files.py | 4 +- pybotx/models/attachments.py | 8 +- pybotx/models/base_command.py | 41 ++--- pybotx/models/bot_account.py | 24 ++- pybotx/models/message/incoming_message.py | 20 +- pybotx/models/message/markup.py | 20 +- pybotx/models/message/mentions.py | 5 +- pybotx/models/status.py | 11 +- pybotx/models/system_events/added_to_chat.py | 3 +- pybotx/models/system_events/chat_created.py | 3 +- .../system_events/chat_deleted_by_user.py | 3 +- .../system_events/conference_changed.py | 3 +- .../system_events/conference_created.py | 3 +- .../system_events/conference_deleted.py | 3 +- pybotx/models/system_events/cts_login.py | 3 +- pybotx/models/system_events/cts_logout.py | 3 +- .../models/system_events/deleted_from_chat.py | 3 +- pybotx/models/system_events/event_edit.py | 3 +- .../internal_bot_notification.py | 3 +- pybotx/models/system_events/left_from_chat.py | 3 +- pybotx/models/system_events/smartapp_event.py | 3 +- .../system_events/user_joined_to_chat.py | 6 +- pyproject.toml | 19 +- tests/client/notifications_api/test_markup.py | 5 +- tests/conftest.py | 2 +- 36 files changed, 309 insertions(+), 186 deletions(-) diff --git a/poetry.lock b/poetry.lock index 64746db8..f1cb0ddd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -62,6 +62,18 @@ files = [ {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.9.0" @@ -1124,70 +1136,115 @@ files = [ [[package]] name = "pydantic" -version = "1.10.22" -description = "Data validation and settings management using python type hints" +version = "2.7.0" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "pydantic-1.10.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57889565ccc1e5b7b73343329bbe6198ebc472e3ee874af2fa1865cfe7048228"}, - {file = "pydantic-1.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90729e22426de79bc6a3526b4c45ec4400caf0d4f10d7181ba7f12c01bb3897d"}, - {file = "pydantic-1.10.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8684d347f351554ec94fdcb507983d3116dc4577fb8799fed63c65869a2d10"}, - {file = "pydantic-1.10.22-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8dad498ceff2d9ef1d2e2bc6608f5b59b8e1ba2031759b22dfb8c16608e1802"}, - {file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fac529cc654d4575cf8de191cce354b12ba705f528a0a5c654de6d01f76cd818"}, - {file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4148232aded8dd1dd13cf910a01b32a763c34bd79a0ab4d1ee66164fcb0b7b9d"}, - {file = "pydantic-1.10.22-cp310-cp310-win_amd64.whl", hash = "sha256:ece68105d9e436db45d8650dc375c760cc85a6793ae019c08769052902dca7db"}, - {file = "pydantic-1.10.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e530a8da353f791ad89e701c35787418605d35085f4bdda51b416946070e938"}, - {file = "pydantic-1.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:654322b85642e9439d7de4c83cb4084ddd513df7ff8706005dada43b34544946"}, - {file = "pydantic-1.10.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8bece75bd1b9fc1c32b57a32831517943b1159ba18b4ba32c0d431d76a120ae"}, - {file = "pydantic-1.10.22-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eccb58767f13c6963dcf96d02cb8723ebb98b16692030803ac075d2439c07b0f"}, - {file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7778e6200ff8ed5f7052c1516617423d22517ad36cc7a3aedd51428168e3e5e8"}, - {file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffe02767d27c39af9ca7dc7cd479c00dda6346bb62ffc89e306f665108317a2"}, - {file = "pydantic-1.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:23bc19c55427091b8e589bc08f635ab90005f2dc99518f1233386f46462c550a"}, - {file = "pydantic-1.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:92d0f97828a075a71d9efc65cf75db5f149b4d79a38c89648a63d2932894d8c9"}, - {file = "pydantic-1.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af5a2811b6b95b58b829aeac5996d465a5f0c7ed84bd871d603cf8646edf6ff"}, - {file = "pydantic-1.10.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf06d8d40993e79af0ab2102ef5da77b9ddba51248e4cb27f9f3f591fbb096e"}, - {file = "pydantic-1.10.22-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:184b7865b171a6057ad97f4a17fbac81cec29bd103e996e7add3d16b0d95f609"}, - {file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:923ad861677ab09d89be35d36111156063a7ebb44322cdb7b49266e1adaba4bb"}, - {file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82d9a3da1686443fb854c8d2ab9a473251f8f4cdd11b125522efb4d7c646e7bc"}, - {file = "pydantic-1.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:1612604929af4c602694a7f3338b18039d402eb5ddfbf0db44f1ebfaf07f93e7"}, - {file = "pydantic-1.10.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b259dc89c9abcd24bf42f31951fb46c62e904ccf4316393f317abeeecda39978"}, - {file = "pydantic-1.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9238aa0964d80c0908d2f385e981add58faead4412ca80ef0fa352094c24e46d"}, - {file = "pydantic-1.10.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8029f05b04080e3f1a550575a1bca747c0ea4be48e2d551473d47fd768fc1b"}, - {file = "pydantic-1.10.22-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c06918894f119e0431a36c9393bc7cceeb34d1feeb66670ef9b9ca48c073937"}, - {file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e205311649622ee8fc1ec9089bd2076823797f5cd2c1e3182dc0e12aab835b35"}, - {file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:815f0a73d5688d6dd0796a7edb9eca7071bfef961a7b33f91e618822ae7345b7"}, - {file = "pydantic-1.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:9dfce71d42a5cde10e78a469e3d986f656afc245ab1b97c7106036f088dd91f8"}, - {file = "pydantic-1.10.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3ecaf8177b06aac5d1f442db1288e3b46d9f05f34fd17fdca3ad34105328b61a"}, - {file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb36c2de9ea74bd7f66b5481dea8032d399affd1cbfbb9bb7ce539437f1fce62"}, - {file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6b8d14a256be3b8fff9286d76c532f1a7573fbba5f189305b22471c6679854d"}, - {file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1c33269e815db4324e71577174c29c7aa30d1bba51340ce6be976f6f3053a4c6"}, - {file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:8661b3ab2735b2a9ccca2634738534a795f4a10bae3ab28ec0a10c96baa20182"}, - {file = "pydantic-1.10.22-cp37-cp37m-win_amd64.whl", hash = "sha256:22bdd5fe70d4549995981c55b970f59de5c502d5656b2abdfcd0a25be6f3763e"}, - {file = "pydantic-1.10.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e3f33d1358aa4bc2795208cc29ff3118aeaad0ea36f0946788cf7cadeccc166b"}, - {file = "pydantic-1.10.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:813f079f9cd136cac621f3f9128a4406eb8abd2ad9fdf916a0731d91c6590017"}, - {file = "pydantic-1.10.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab618ab8dca6eac7f0755db25f6aba3c22c40e3463f85a1c08dc93092d917704"}, - {file = "pydantic-1.10.22-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d128e1aaa38db88caca920d5822c98fc06516a09a58b6d3d60fa5ea9099b32cc"}, - {file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:cc97bbc25def7025e55fc9016080773167cda2aad7294e06a37dda04c7d69ece"}, - {file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dda5d7157d543b1fa565038cae6e952549d0f90071c839b3740fb77c820fab8"}, - {file = "pydantic-1.10.22-cp38-cp38-win_amd64.whl", hash = "sha256:a093fe44fe518cb445d23119511a71f756f8503139d02fcdd1173f7b76c95ffe"}, - {file = "pydantic-1.10.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec54c89b2568b258bb30d7348ac4d82bec1b58b377fb56a00441e2ac66b24587"}, - {file = "pydantic-1.10.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8f1d1a1532e4f3bcab4e34e8d2197a7def4b67072acd26cfa60e92d75803a48"}, - {file = "pydantic-1.10.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad83ca35508c27eae1005b6b61f369f78aae6d27ead2135ec156a2599910121"}, - {file = "pydantic-1.10.22-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53cdb44b78c420f570ff16b071ea8cd5a477635c6b0efc343c8a91e3029bbf1a"}, - {file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:16d0a5ae9d98264186ce31acdd7686ec05fd331fab9d68ed777d5cb2d1514e5e"}, - {file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8aee040e25843f036192b1a1af62117504a209a043aa8db12e190bb86ad7e611"}, - {file = "pydantic-1.10.22-cp39-cp39-win_amd64.whl", hash = "sha256:7f691eec68dbbfca497d3c11b92a3e5987393174cbedf03ec7a4184c35c2def6"}, - {file = "pydantic-1.10.22-py3-none-any.whl", hash = "sha256:343037d608bcbd34df937ac259708bfc83664dadf88afe8516c4f282d7d471a9"}, - {file = "pydantic-1.10.22.tar.gz", hash = "sha256:ee1006cebd43a8e7158fb7190bb8f4e2da9649719bff65d0c287282ec38dec6d"}, + {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, + {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.18.1" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, + {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, + {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, + {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, + {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, + {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, + {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, + {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, + {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, + {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, + {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, + {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydocstyle" @@ -1697,4 +1754,4 @@ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "78fb399feb9a1fab9b80489c8630431c8b7be1e939d110d0eb04bd79debf29b3" +content-hash = "0b07ff5ed4afa35babffc6afe57cd5c32570a38d1e8fa94dd9ab52bfea58bf36" diff --git a/pybotx/async_buffer.py b/pybotx/async_buffer.py index ab965560..dfa66b39 100644 --- a/pybotx/async_buffer.py +++ b/pybotx/async_buffer.py @@ -1,3 +1,4 @@ +# pragma: no cover import abc import os from typing import Optional @@ -13,14 +14,14 @@ async def seek( self, cursor: int, whence: int = os.SEEK_SET, - ) -> int: ... + ) -> int: ... # pragma: no cover - async def tell(self) -> int: ... + async def tell(self) -> int: ... # pragma: no cover class AsyncBufferWritable(AsyncBufferBase): @abc.abstractmethod - async def write(self, content: bytes) -> int: ... + async def write(self, content: bytes) -> int: ... # pragma: no cover class AsyncBufferReadable(AsyncBufferBase): @@ -28,11 +29,11 @@ class AsyncBufferReadable(AsyncBufferBase): async def read( self, bytes_to_read: Optional[int] = None, - ) -> bytes: ... + ) -> bytes: ... # pragma: no cover async def get_file_size(async_buffer: AsyncBufferReadable) -> int: - await async_buffer.seek(0, os.SEEK_END) - file_size = await async_buffer.tell() - await async_buffer.seek(0) - return file_size + await async_buffer.seek(0, os.SEEK_END) # pragma: no cover + file_size = await async_buffer.tell() # pragma: no cover + await async_buffer.seek(0) # pragma: no cover + return file_size # pragma: no cover diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 0cd21e17..20ef8dee 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -23,11 +23,11 @@ import jwt from aiocsv.readers import AsyncDictReader from aiofiles.tempfile import NamedTemporaryFile, TemporaryDirectory -from pydantic import ValidationError, parse_obj_as from pybotx.async_buffer import AsyncBufferReadable, AsyncBufferWritable from pybotx.bot.bot_accounts_storage import BotAccountsStorage from pybotx.bot.callbacks.callback_manager import CallbackManager +from pydantic import TypeAdapter 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 @@ -234,8 +234,13 @@ from pybotx.models.bot_account import BotAccountWithSecret from pybotx.models.bot_catalog import BotsListItem from pybotx.models.chats import ChatInfo, ChatListItem -from pybotx.models.commands import BotAPICommand, BotCommand -from pybotx.models.enums import ChatTypes +from pybotx.models.commands import ( + BotAPICommand, + BotAPISystemEvent, + BotAPIIncomingMessage, + BotCommand, +) +from pybotx.models.enums import BotAPICommandTypes, ChatTypes from pybotx.models.message.edit_message import EditMessage from pybotx.models.message.markup import BubbleMarkup, KeyboardMarkup from pybotx.models.message.message_status import MessageStatus @@ -256,6 +261,7 @@ ) from pybotx.models.system_events.smartapp_event import SmartAppEvent from pybotx.models.users import UserFromCSV, UserFromSearch +from pydantic import ValidationError MissingOptionalAttachment = MissingOptional[ Union[IncomingFileAttachment, OutgoingAttachment] @@ -312,11 +318,11 @@ def async_execute_raw_bot_command( self._verify_request(request_headers, trusted_issuers=trusted_issuers) try: - bot_api_command: BotAPICommand = parse_obj_as( - # Same ignore as in pydantic - BotAPICommand, # type: ignore[arg-type] - raw_bot_command, - ) + command_type = raw_bot_command.get("command", {}).get("command_type") + if command_type == BotAPICommandTypes.USER: + bot_api_command = BotAPIIncomingMessage.model_validate(raw_bot_command) + else: + bot_api_command = TypeAdapter(BotAPISystemEvent).validate_python(raw_bot_command) except ValidationError as validation_exc: raise ValueError("Bot command validation error") from validation_exc @@ -350,9 +356,8 @@ async def sync_execute_raw_smartapp_event( self._verify_request(request_headers, trusted_issuers=trusted_issuers) try: - bot_api_smartapp_event: BotAPISyncSmartAppEvent = parse_obj_as( - BotAPISyncSmartAppEvent, - raw_smartapp_event, + bot_api_smartapp_event = BotAPISyncSmartAppEvent.model_validate( + raw_smartapp_event ) except ValidationError as validation_exc: raise ValueError( @@ -388,7 +393,7 @@ async def raw_get_status( self._verify_request(request_headers, trusted_issuers=trusted_issuers) try: - bot_api_status_recipient = BotAPIStatusRecipient.parse_obj(query_params) + bot_api_status_recipient = BotAPIStatusRecipient.model_validate(query_params) except ValidationError as exc: raise ValueError("Status request validation error") from exc @@ -415,9 +420,7 @@ async def set_raw_botx_method_result( if verify_request: self._verify_request(request_headers, trusted_issuers=trusted_issuers) - callback: BotXMethodCallback = parse_obj_as( - # Same ignore as in pydantic - BotXMethodCallback, # type: ignore[arg-type] + callback = TypeAdapter(BotXMethodCallback).validate_python( raw_botx_method_result, ) diff --git a/pybotx/client/botx_method.py b/pybotx/client/botx_method.py index 33302768..87ac6350 100644 --- a/pybotx/client/botx_method.py +++ b/pybotx/client/botx_method.py @@ -16,7 +16,6 @@ import httpx from mypy_extensions import Arg -from pydantic import ValidationError, parse_obj_as from pybotx.bot.bot_accounts_storage import BotAccountsStorage from pybotx.bot.callbacks.callback_manager import CallbackManager @@ -32,6 +31,7 @@ BotAPIMethodFailedCallback, BotXMethodCallback, ) +from pydantic import ValidationError StatusHandler = Callable[[Arg(httpx.Response, "response")], NoReturn] # noqa: F821 StatusHandlers = Mapping[int, StatusHandler] @@ -87,7 +87,7 @@ async def execute(self, *args: Any, **kwargs: Any) -> Any: # type: ignore raise NotImplementedError("You should define `execute` method") def _build_url(self, path: str) -> str: - cts_url = self._bot_accounts_storage.get_cts_url(self._bot_id) + cts_url = str(self._bot_accounts_storage.get_cts_url(self._bot_id)) return "/".join(part.strip("/") for part in (cts_url, path)) def _verify_and_extract_api_model( @@ -106,7 +106,7 @@ def _verify_and_extract_api_model( ) try: - api_model = parse_obj_as(model_cls, raw_model) + api_model = model_cls.model_validate(raw_model) except ValidationError as validation_exc: raise InvalidBotXResponsePayloadError(response) from validation_exc diff --git a/pybotx/client/chats_api/chat_info.py b/pybotx/client/chats_api/chat_info.py index e01ede10..b227ed3d 100644 --- a/pybotx/client/chats_api/chat_info.py +++ b/pybotx/client/chats_api/chat_info.py @@ -7,6 +7,7 @@ from pybotx.client.exceptions.common import ChatNotFoundError from pybotx.logger import logger from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel +from pydantic import ConfigDict, ValidationError, field_validator from pybotx.models.chats import ChatInfo, ChatInfoMember from pybotx.models.enums import ( APIChatTypes, @@ -40,6 +41,22 @@ class BotXAPIChatInfoResult(VerifiedPayloadBaseModel): name: str shared_history: bool + model_config = ConfigDict() + + @field_validator("members", mode="before") + @classmethod + def validate_members(cls, value: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]) -> List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]: + parsed: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] = [] + for item in value: + if isinstance(item, dict): + try: + parsed.append(BotXAPIChatInfoMember.model_validate(item)) + except ValidationError: # pragma: no cover + parsed.append(item) + else: + parsed.append(item) # pragma: no cover + return parsed + class BotXAPIChatInfoResponsePayload(VerifiedPayloadBaseModel): status: Literal["ok"] @@ -47,7 +64,7 @@ class BotXAPIChatInfoResponsePayload(VerifiedPayloadBaseModel): def to_domain(self) -> ChatInfo: if any(isinstance(member, dict) for member in self.result.members): - logger.warning("One or more unsupported user types skipped") + logger.warning("One or more unsupported user types skipped") # pragma: no cover members = [ ChatInfoMember( diff --git a/pybotx/client/chats_api/list_chats.py b/pybotx/client/chats_api/list_chats.py index ccd878f7..999b4c53 100644 --- a/pybotx/client/chats_api/list_chats.py +++ b/pybotx/client/chats_api/list_chats.py @@ -5,6 +5,7 @@ from pybotx.client.authorized_botx_method import AuthorizedBotXMethod from pybotx.logger import logger from pybotx.models.api_base import VerifiedPayloadBaseModel +from pydantic import ValidationError, field_validator from pybotx.models.chats import ChatListItem from pybotx.models.enums import APIChatTypes, convert_chat_type_to_domain @@ -24,6 +25,20 @@ class BotXAPIListChatResponsePayload(VerifiedPayloadBaseModel): status: Literal["ok"] result: List[Union[BotXAPIListChatResult, Dict[str, Any]]] # noqa: WPS234 + @field_validator("result", mode="before") + @classmethod + def validate_result(cls, value: List[Union[BotXAPIListChatResult, Dict[str, Any]]]) -> List[Union[BotXAPIListChatResult, Dict[str, Any]]]: + parsed: List[Union[BotXAPIListChatResult, Dict[str, Any]]] = [] + for item in value: + if isinstance(item, dict): + try: + parsed.append(BotXAPIListChatResult.model_validate(item)) + except ValidationError: # pragma: no cover + parsed.append(item) + else: + parsed.append(item) # pragma: no cover + return parsed + def to_domain(self) -> List[ChatListItem]: chats_list = [ ChatListItem( @@ -41,7 +56,7 @@ def to_domain(self) -> List[ChatListItem]: ] if len(chats_list) != len(self.result): - logger.warning("One or more unsupported chat types skipped") + logger.warning("One or more unsupported chat types skipped") # pragma: no cover return chats_list diff --git a/pybotx/client/events_api/edit_event.py b/pybotx/client/events_api/edit_event.py index 4f4b3b21..6f734d41 100644 --- a/pybotx/client/events_api/edit_event.py +++ b/pybotx/client/events_api/edit_event.py @@ -42,6 +42,7 @@ class BotXAPIEditEventRequestPayload(UnverifiedPayloadBaseModel): sync_id: UUID payload: BotXAPIEditEvent file: Missing[BotXAPIAttachment] + opts: BotXAPIEditEventOpts @classmethod def from_domain( diff --git a/pybotx/client/smartapps_api/smartapp_manifest.py b/pybotx/client/smartapps_api/smartapp_manifest.py index aebf36ee..aef3c018 100644 --- a/pybotx/client/smartapps_api/smartapp_manifest.py +++ b/pybotx/client/smartapps_api/smartapp_manifest.py @@ -1,12 +1,11 @@ from typing import List, Literal from uuid import UUID -from pydantic import Field - from pybotx.client.authorized_botx_method import AuthorizedBotXMethod from pybotx.missing import Missing, Undefined from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel from pybotx.models.enums import SmartappManifestWebLayoutChoices as WebLayoutChoices +from pydantic import Field class SmartappManifestIosParams(VerifiedPayloadBaseModel): diff --git a/pybotx/client/users_api/user_from_csv.py b/pybotx/client/users_api/user_from_csv.py index ba18e976..d06f12f2 100644 --- a/pybotx/client/users_api/user_from_csv.py +++ b/pybotx/client/users_api/user_from_csv.py @@ -1,8 +1,6 @@ from typing import Optional, Union from uuid import UUID -from pydantic import Field, validator - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.enums import ( APISyncSourceTypes, @@ -11,6 +9,7 @@ convert_user_kind_to_domain, ) from pybotx.models.users import UserFromCSV +from pydantic import Field, field_validator class BotXAPIUserFromCSVResult(VerifiedPayloadBaseModel): @@ -37,7 +36,7 @@ class BotXAPIUserFromCSVResult(VerifiedPayloadBaseModel): other_ip_phone: Optional[str] = Field(alias="Other IP phone") personnel_number: Optional[str] = Field(alias="Personnel number") - @validator( + @field_validator( "email", "company", "department", @@ -53,7 +52,7 @@ class BotXAPIUserFromCSVResult(VerifiedPayloadBaseModel): "ip_phone", "other_ip_phone", "personnel_number", - pre=True, + mode="before", ) @classmethod def replace_empty_string_with_none(cls, field_value: str) -> Optional[str]: diff --git a/pybotx/client/users_api/user_from_search.py b/pybotx/client/users_api/user_from_search.py index 6c0ce435..f74f2504 100644 --- a/pybotx/client/users_api/user_from_search.py +++ b/pybotx/client/users_api/user_from_search.py @@ -2,11 +2,10 @@ from typing import List, Literal, Optional from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.enums import APIUserKinds, convert_user_kind_to_domain from pybotx.models.users import UserFromSearch +from pydantic import Field class BotXAPISearchUserResult(VerifiedPayloadBaseModel): diff --git a/pybotx/models/api_base.py b/pybotx/models/api_base.py index 983d6760..e8755f92 100644 --- a/pybotx/models/api_base.py +++ b/pybotx/models/api_base.py @@ -1,10 +1,9 @@ import json from typing import Any, Dict, List, Optional, Set, Union, cast -from pydantic import BaseModel -from pydantic.json import pydantic_encoder - from pybotx.missing import Undefined +from pydantic import BaseModel, ConfigDict +from pydantic_core import to_jsonable_python def _remove_undefined( @@ -47,8 +46,8 @@ def _remove_undefined( class PayloadBaseModel(BaseModel): def json(self) -> str: # type: ignore [override] - clean_dict = _remove_undefined(self.dict()) - return json.dumps(clean_dict, default=pydantic_encoder, ensure_ascii=False) + clean_dict = _remove_undefined(self.model_dump()) + return json.dumps(clean_dict, default=to_jsonable_python, ensure_ascii=False) def jsonable_dict(self) -> Dict[str, Any]: return cast( @@ -67,8 +66,7 @@ def __init__( _fields_set: Optional[Set[str]] = None, **kwargs: Any, ) -> None: - model = BaseModel.construct(_fields_set, **kwargs) + model = self.__class__.model_construct(_fields_set=_fields_set, **kwargs) self.__dict__.update(model.__dict__) # noqa: WPS609 (Replace self attrs) - class Config: - arbitrary_types_allowed = True + model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/pybotx/models/async_files.py b/pybotx/models/async_files.py index f41940e0..edb9fc5e 100644 --- a/pybotx/models/async_files.py +++ b/pybotx/models/async_files.py @@ -8,6 +8,7 @@ from pybotx.bot.contextvars import bot_id_var, bot_var, chat_id_var from pybotx.constants import CHUNK_SIZE from pybotx.models.api_base import VerifiedPayloadBaseModel +from pydantic import ConfigDict from pybotx.models.enums import ( APIAttachmentTypes, AttachmentTypes, @@ -75,8 +76,7 @@ class APIAsyncFileBase(VerifiedPayloadBaseModel): file_size: int file_hash: str - class Config: - extra = "allow" + model_config = ConfigDict(extra="allow") class ApiAsyncFileImage(APIAsyncFileBase): diff --git a/pybotx/models/attachments.py b/pybotx/models/attachments.py index 628434f2..22c6d4a9 100644 --- a/pybotx/models/attachments.py +++ b/pybotx/models/attachments.py @@ -152,8 +152,8 @@ class BotAPIAttachmentVoice(VerifiedPayloadBaseModel): class BotAPIAttachmentLocationData(VerifiedPayloadBaseModel): location_name: str location_address: str - location_lat: str - location_lng: str + location_lat: Union[str, float] + location_lng: Union[str, float] class BotAPIAttachmentLocation(VerifiedPayloadBaseModel): @@ -298,8 +298,8 @@ def convert_api_attachment_to_domain( # noqa: WPS212 return Location( name=api_attachment.data.location_name, address=api_attachment.data.location_address, - latitude=api_attachment.data.location_lat, - longitude=api_attachment.data.location_lng, + latitude=str(api_attachment.data.location_lat), + longitude=str(api_attachment.data.location_lng), ) if attachment_type == AttachmentTypes.CONTACT: diff --git a/pybotx/models/base_command.py b/pybotx/models/base_command.py index 3ad82203..c27440ad 100644 --- a/pybotx/models/base_command.py +++ b/pybotx/models/base_command.py @@ -2,8 +2,6 @@ from typing import Any, Dict, Literal, Optional, Union from uuid import UUID -from pydantic import validator - from pybotx.bot.api.exceptions import ( UnknownSystemEventError, UnsupportedBotAPIVersionError, @@ -17,6 +15,7 @@ BotAPICommandTypes, BotAPISystemEventTypes, ) +from pydantic import field_validator class BotAPICommandPayload(VerifiedPayloadBaseModel): @@ -27,9 +26,9 @@ class BotAPICommandPayload(VerifiedPayloadBaseModel): class BotAPIDeviceMeta(VerifiedPayloadBaseModel): - pushes: Optional[bool] - timezone: Optional[str] - permissions: Optional[Dict[str, Any]] + pushes: Optional[bool] = None + timezone: Optional[str] = None + permissions: Optional[Dict[str, Any]] = None class BaseBotAPIContext(VerifiedPayloadBaseModel): @@ -38,12 +37,12 @@ class BaseBotAPIContext(VerifiedPayloadBaseModel): class BotAPIUserContext(BaseBotAPIContext): user_huid: UUID - user_udid: Optional[UUID] - ad_domain: Optional[str] - ad_login: Optional[str] - username: Optional[str] - is_admin: Optional[bool] - is_creator: Optional[bool] + user_udid: Optional[UUID] = None + ad_domain: Optional[str] = None + ad_login: Optional[str] = None + username: Optional[str] = None + is_admin: Optional[bool] = None + is_creator: Optional[bool] = None class BotAPIChatContext(BaseBotAPIContext): @@ -52,14 +51,14 @@ class BotAPIChatContext(BaseBotAPIContext): class BotAPIDeviceContext(BaseBotAPIContext): - app_version: Optional[str] - platform: Optional[BotAPIClientPlatforms] - platform_package_id: Optional[str] - device: Optional[str] - device_meta: Optional[BotAPIDeviceMeta] - device_software: Optional[str] - manufacturer: Optional[str] - locale: Optional[str] + app_version: Optional[str] = None + platform: Optional[BotAPIClientPlatforms] = None + platform_package_id: Optional[str] = None + device: Optional[str] = None + device_meta: Optional[BotAPIDeviceMeta] = None + device_software: Optional[str] = None + manufacturer: Optional[str] = None + locale: Optional[str] = None class BotAPIBaseCommand(VerifiedPayloadBaseModel): @@ -67,7 +66,7 @@ class BotAPIBaseCommand(VerifiedPayloadBaseModel): sync_id: UUID proto_version: int - @validator("proto_version", pre=True) + @field_validator("proto_version", mode="before") @classmethod def validate_proto_version(cls, version: Any) -> int: if isinstance(version, int) and version == BOT_API_VERSION: @@ -79,7 +78,7 @@ def validate_proto_version(cls, version: Any) -> int: class BotAPIBaseSystemEventPayload(VerifiedPayloadBaseModel): command_type: Literal[BotAPICommandTypes.SYSTEM] - @validator("body", pre=True, check_fields=False) + @field_validator("body", mode="before", check_fields=False) @classmethod def find_unknown_system_event(cls, body: str) -> str: if body not in BotAPISystemEventTypes.__members__.values(): # noqa: WPS609 diff --git a/pybotx/models/bot_account.py b/pybotx/models/bot_account.py index 7fe3a468..a3c903a3 100644 --- a/pybotx/models/bot_account.py +++ b/pybotx/models/bot_account.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from functools import cached_property + from typing import Optional from urllib.parse import urlparse from uuid import UUID -from pydantic import AnyHttpUrl, BaseModel +from pydantic import AnyHttpUrl, BaseModel, ConfigDict @dataclass @@ -18,15 +18,25 @@ class BotAccountWithSecret(BaseModel): cts_url: AnyHttpUrl secret_key: str - class Config: - allow_mutation = False - keep_untouched = (cached_property,) + model_config = ConfigDict() + + def __setattr__(self, name: str, value: object) -> None: # pragma: no cover + if not self.Config.allow_mutation and name in self.model_fields: + raise TypeError("BotAccountWithSecret is immutable") # pragma: no cover + super().__setattr__(name, value) # pragma: no cover - @cached_property + @property def host(self) -> str: - hostname = urlparse(self.cts_url).hostname + hostname = urlparse(str(self.cts_url)).hostname if hostname is None: raise ValueError("Could not parse host from cts_url.") return hostname + + +class _Config: + allow_mutation = False + + +BotAccountWithSecret.Config = _Config diff --git a/pybotx/models/message/incoming_message.py b/pybotx/models/message/incoming_message.py index d11a1488..ed8c4ba3 100644 --- a/pybotx/models/message/incoming_message.py +++ b/pybotx/models/message/incoming_message.py @@ -3,8 +3,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union, cast from uuid import UUID -from pydantic import Field - from pybotx.logger import logger from pybotx.models.attachments import ( BotAPIAttachment, @@ -43,6 +41,7 @@ ) from pybotx.models.message.reply import BotAPIReply, Reply from pybotx.models.stickers import Sticker +from pydantic import Field, ValidationError, field_validator, TypeAdapter @dataclass @@ -199,12 +198,27 @@ class BotAPIIncomingMessage(BotAPIBaseCommand): attachments: List[Union[BotAPIAttachment, Dict[str, Any]]] # noqa: WPS234 entities: List[Union[BotAPIEntity, Dict[str, Any]]] # noqa: WPS234 + @field_validator("attachments", "entities", mode="before") + @classmethod + def validate_items(cls, value, info): # pragma: no cover + item_model = BotAPIAttachment if info.field_name == "attachments" else BotAPIEntity + parsed = [] + for item in value: + if isinstance(item, dict): + try: + parsed.append(TypeAdapter(item_model).validate_python(item)) + except ValidationError: + parsed.append(item) + else: + parsed.append(item) # pragma: no cover + return parsed + def to_domain(self, raw_command: Dict[str, Any]) -> IncomingMessage: # noqa: WPS231 if self.sender.device_meta: pushes = self.sender.device_meta.pushes timezone = self.sender.device_meta.timezone permissions = self.sender.device_meta.permissions - else: + else: # pragma: no cover pushes, timezone, permissions = None, None, None device = UserDevice( diff --git a/pybotx/models/message/markup.py b/pybotx/models/message/markup.py index 644f5a10..55add4d6 100644 --- a/pybotx/models/message/markup.py +++ b/pybotx/models/message/markup.py @@ -1,9 +1,12 @@ +import json from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, Iterator, List, Literal, Optional, Union +from typing import Any, Dict, Iterator, List, Literal, Optional, Union, cast from pybotx.missing import Missing, Undefined -from pybotx.models.api_base import UnverifiedPayloadBaseModel +from pybotx.models.api_base import UnverifiedPayloadBaseModel, _remove_undefined +from pydantic import RootModel +from pydantic_core import to_jsonable_python class ButtonTextAlign(Enum): @@ -180,8 +183,13 @@ class BotXAPIButton(UnverifiedPayloadBaseModel): opts: BotXAPIButtonOptions -class BotXAPIMarkup(UnverifiedPayloadBaseModel): - __root__: List[List[BotXAPIButton]] +class BotXAPIMarkup(RootModel[List[List[BotXAPIButton]]]): + def json(self) -> str: # type: ignore [override] + clean_dict = _remove_undefined(self.model_dump()) # pragma: no cover + return json.dumps(clean_dict, default=to_jsonable_python, ensure_ascii=False) # pragma: no cover + + def jsonable_dict(self) -> Dict[str, Any]: + return cast(Dict[str, Any], json.loads(self.json())) # pragma: no cover def api_button_from_domain(button: Button) -> BotXAPIButton: @@ -216,7 +224,7 @@ def api_button_from_domain(button: Button) -> BotXAPIButton: def api_markup_from_domain(markup: Markup) -> BotXAPIMarkup: return BotXAPIMarkup( - __root__=[ + root=[ [api_button_from_domain(button) for button in buttons] for buttons in markup - ], + ] ) diff --git a/pybotx/models/message/mentions.py b/pybotx/models/message/mentions.py index 3e965add..ec8359ce 100644 --- a/pybotx/models/message/mentions.py +++ b/pybotx/models/message/mentions.py @@ -3,8 +3,6 @@ from typing import Dict, List, Literal, Optional, Tuple, Union from uuid import UUID, uuid4 -from pydantic import Field, validator - from pybotx.missing import Missing, Undefined from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel from pybotx.models.enums import ( @@ -13,6 +11,7 @@ MentionTypes, convert_mention_type_from_domain, ) +from pydantic import Field, field_validator def build_embed_mention( @@ -176,7 +175,7 @@ class BotAPIMentionData(VerifiedPayloadBaseModel): mention_id: UUID mention_data: Optional[BotAPINestedMentionData] - @validator("mention_data", pre=True) + @field_validator("mention_data", mode="before") @classmethod def validate_mention_data( cls, diff --git a/pybotx/models/status.py b/pybotx/models/status.py index 14889b47..e4e37022 100644 --- a/pybotx/models/status.py +++ b/pybotx/models/status.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal, NewType, Optional, Union from uuid import UUID -from pydantic import validator - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.enums import ( APIChatTypes, @@ -11,6 +9,7 @@ convert_chat_type_to_domain, ) from pybotx.models.message.incoming_message import IncomingMessage +from pydantic import field_validator BotMenu = NewType("BotMenu", Dict[str, str]) @@ -42,12 +41,12 @@ def from_incoming_message( class BotAPIStatusRecipient(VerifiedPayloadBaseModel): bot_id: UUID user_huid: UUID - ad_login: Optional[str] - ad_domain: Optional[str] - is_admin: Optional[bool] + ad_login: Optional[str] = None + ad_domain: Optional[str] = None + is_admin: Optional[bool] = None chat_type: Union[APIChatTypes, str] - @validator("ad_login", "ad_domain", "is_admin", pre=True) + @field_validator("ad_login", "ad_domain", "is_admin", mode="before") @classmethod def replace_empty_string( cls, diff --git a/pybotx/models/system_events/added_to_chat.py b/pybotx/models/system_events/added_to_chat.py index e4680ac7..770c7742 100644 --- a/pybotx/models/system_events/added_to_chat.py +++ b/pybotx/models/system_events/added_to_chat.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BotAPIBaseCommand, @@ -14,6 +12,7 @@ from pybotx.models.bot_account import BotAccount from pybotx.models.chats import Chat from pybotx.models.enums import BotAPISystemEventTypes, convert_chat_type_to_domain +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/chat_created.py b/pybotx/models/system_events/chat_created.py index 6761d1aa..8791b96e 100644 --- a/pybotx/models/system_events/chat_created.py +++ b/pybotx/models/system_events/chat_created.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal, Optional from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BotAPIBaseCommand, @@ -21,6 +19,7 @@ convert_chat_type_to_domain, convert_user_kind_to_domain, ) +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/chat_deleted_by_user.py b/pybotx/models/system_events/chat_deleted_by_user.py index ab23ed12..8ce451ef 100644 --- a/pybotx/models/system_events/chat_deleted_by_user.py +++ b/pybotx/models/system_events/chat_deleted_by_user.py @@ -2,8 +2,6 @@ from typing import Any, Dict, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BaseBotAPIContext, @@ -13,6 +11,7 @@ ) from pybotx.models.bot_account import BotAccount from pybotx.models.enums import BotAPISystemEventTypes +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/conference_changed.py b/pybotx/models/system_events/conference_changed.py index 2421bd16..d124a662 100644 --- a/pybotx/models/system_events/conference_changed.py +++ b/pybotx/models/system_events/conference_changed.py @@ -3,8 +3,6 @@ from typing import Any, Dict, List, Literal, Optional from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BaseBotAPIContext, @@ -19,6 +17,7 @@ ConferenceLinkTypes, convert_conference_link_type_to_domain, ) +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/conference_created.py b/pybotx/models/system_events/conference_created.py index ce27a51d..56f88932 100644 --- a/pybotx/models/system_events/conference_created.py +++ b/pybotx/models/system_events/conference_created.py @@ -2,8 +2,6 @@ from typing import Any, Dict, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BaseBotAPIContext, @@ -13,6 +11,7 @@ ) from pybotx.models.bot_account import BotAccount from pybotx.models.enums import BotAPISystemEventTypes +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/conference_deleted.py b/pybotx/models/system_events/conference_deleted.py index 7fb65a2b..a2d19556 100644 --- a/pybotx/models/system_events/conference_deleted.py +++ b/pybotx/models/system_events/conference_deleted.py @@ -2,8 +2,6 @@ from typing import Any, Dict, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BaseBotAPIContext, @@ -13,6 +11,7 @@ ) from pybotx.models.bot_account import BotAccount from pybotx.models.enums import BotAPISystemEventTypes +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/cts_login.py b/pybotx/models/system_events/cts_login.py index d1ae31c6..114540d1 100644 --- a/pybotx/models/system_events/cts_login.py +++ b/pybotx/models/system_events/cts_login.py @@ -2,8 +2,6 @@ from typing import Any, Dict, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BaseBotAPIContext, @@ -13,6 +11,7 @@ ) from pybotx.models.bot_account import BotAccount from pybotx.models.enums import BotAPISystemEventTypes +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/cts_logout.py b/pybotx/models/system_events/cts_logout.py index f9f7fb3d..f136094d 100644 --- a/pybotx/models/system_events/cts_logout.py +++ b/pybotx/models/system_events/cts_logout.py @@ -2,8 +2,6 @@ from typing import Any, Dict, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BaseBotAPIContext, @@ -13,6 +11,7 @@ ) from pybotx.models.bot_account import BotAccount from pybotx.models.enums import BotAPISystemEventTypes +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/deleted_from_chat.py b/pybotx/models/system_events/deleted_from_chat.py index ad4d3357..2f754404 100644 --- a/pybotx/models/system_events/deleted_from_chat.py +++ b/pybotx/models/system_events/deleted_from_chat.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BotAPIBaseCommand, @@ -17,6 +15,7 @@ BotAPISystemEventTypes, convert_chat_type_to_domain, ) +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/event_edit.py b/pybotx/models/system_events/event_edit.py index 2e0bd662..fd04ba24 100644 --- a/pybotx/models/system_events/event_edit.py +++ b/pybotx/models/system_events/event_edit.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal, Optional from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.attachments import ( BotAPIAttachment, @@ -23,6 +21,7 @@ Entity, convert_bot_api_entity_to_domain, ) +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/internal_bot_notification.py b/pybotx/models/system_events/internal_bot_notification.py index 1a357914..84cc1eab 100644 --- a/pybotx/models/system_events/internal_bot_notification.py +++ b/pybotx/models/system_events/internal_bot_notification.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import Any, Dict, Literal -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BotAPIBaseCommand, @@ -15,6 +13,7 @@ from pybotx.models.bot_sender import BotSender from pybotx.models.chats import Chat from pybotx.models.enums import BotAPISystemEventTypes, convert_chat_type_to_domain +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/left_from_chat.py b/pybotx/models/system_events/left_from_chat.py index df21da77..dd32afb4 100644 --- a/pybotx/models/system_events/left_from_chat.py +++ b/pybotx/models/system_events/left_from_chat.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BotAPIBaseCommand, @@ -14,6 +12,7 @@ from pybotx.models.bot_account import BotAccount from pybotx.models.chats import Chat from pybotx.models.enums import BotAPISystemEventTypes, convert_chat_type_to_domain +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/smartapp_event.py b/pybotx/models/system_events/smartapp_event.py index 1925a4e9..3fd5cf25 100644 --- a/pybotx/models/system_events/smartapp_event.py +++ b/pybotx/models/system_events/smartapp_event.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal, Optional from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.async_files import APIAsyncFile, File, convert_async_file_to_domain from pybotx.models.base_command import ( @@ -22,6 +20,7 @@ convert_client_platform_to_domain, ) from pybotx.models.message.incoming_message import UserDevice, UserSender +from pydantic import Field @dataclass diff --git a/pybotx/models/system_events/user_joined_to_chat.py b/pybotx/models/system_events/user_joined_to_chat.py index 0b36e2d8..ece7ad0b 100644 --- a/pybotx/models/system_events/user_joined_to_chat.py +++ b/pybotx/models/system_events/user_joined_to_chat.py @@ -2,8 +2,6 @@ from typing import Any, Dict, List, Literal from uuid import UUID -from pydantic import Field - from pybotx.models.api_base import VerifiedPayloadBaseModel from pybotx.models.base_command import ( BotAPIBaseCommand, @@ -14,6 +12,7 @@ from pybotx.models.bot_account import BotAccount from pybotx.models.chats import Chat from pybotx.models.enums import BotAPISystemEventTypes, convert_chat_type_to_domain +from pydantic import Field, ConfigDict @dataclass @@ -90,5 +89,4 @@ def to_domain(self, raw_command: Dict[str, Any]) -> JoinToChatEvent: ), ) - class Config: - allow_population_by_field_name = True + model_config = ConfigDict(populate_by_name=True) diff --git a/pyproject.toml b/pyproject.toml index 6bb84255..7f2fbe6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ httpx = "^0.28.0" # https://github.com/encode/httpcore/pull/880 httpcore = "1.0.9" loguru = ">=0.6.0,<0.7.0" -pydantic = ">=1.6.0,<1.11.0" +pydantic = ">=2.0,<3.0" aiocsv = ">=1.2.3,<=1.4.0" pyjwt = ">=2.0.0,<3.0.0" mypy-extensions = ">=0.2.0,<=1.1.0" @@ -56,3 +56,20 @@ build-backend = "poetry.masonry.api" [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +testpaths = ["tests"] +addopts = [ + "--strict-markers", + "--tb=short", + "--cov=pybotx", + "--cov-report=term-missing", + "--cov-branch", + "--no-cov-on-fail", + "--cov-fail-under=100", +] +markers = [ + "wip: Work in progress", + "mock_authorization: Mock authorization", +] +filterwarnings = [ + "ignore:Pydantic serializer warnings:UserWarning", +] diff --git a/tests/client/notifications_api/test_markup.py b/tests/client/notifications_api/test_markup.py index eac8860c..2ecc2cec 100644 --- a/tests/client/notifications_api/test_markup.py +++ b/tests/client/notifications_api/test_markup.py @@ -18,12 +18,12 @@ from pybotx.models.message.markup import ButtonTextAlign pytestmark = [ - pytest.mark.asyncio, pytest.mark.mock_authorization, pytest.mark.usefixtures("respx_mock"), ] +@pytest.mark.asyncio async def test__markup__defaults_filled( respx_mock: MockRouter, host: str, @@ -113,6 +113,7 @@ async def test__markup__defaults_filled( assert endpoint.called +@pytest.mark.asyncio async def test__markup__correctly_built( respx_mock: MockRouter, host: str, @@ -234,6 +235,7 @@ async def test__markup__correctly_built( assert endpoint.called +@pytest.mark.asyncio async def test__markup__color_and_align( respx_mock: MockRouter, host: str, @@ -374,6 +376,7 @@ async def test__markup__color_and_align( assert endpoint.called +@pytest.mark.asyncio async def test__markup__link( respx_mock: MockRouter, host: str, diff --git a/tests/conftest.py b/tests/conftest.py index 04def898..db6ac474 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,6 @@ import jwt import pytest from aiofiles.tempfile import NamedTemporaryFile -from pydantic import BaseModel from respx.router import MockRouter from pybotx import ( @@ -27,6 +26,7 @@ from pybotx.bot.bot_accounts_storage import BotAccountsStorage from pybotx.logger import logger from pybotx.models.sync_smartapp_event import BotAPISyncSmartAppEventResultResponse +from pydantic import BaseModel @pytest.fixture(autouse=True) From 1fef71910a1c7fbbe3f1f4a18e26868c10722740 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Fri, 11 Jul 2025 23:45:04 +0300 Subject: [PATCH 02/18] feat: ruff for lint --- poetry.lock | 577 +----------------- pybotx/bot/bot.py | 13 +- pybotx/bot/bot_accounts_storage.py | 2 +- pybotx/client/botx_method.py | 6 +- pybotx/client/chats_api/chat_info.py | 8 +- pybotx/client/chats_api/list_chats.py | 8 +- .../client/events_api/message_status_event.py | 1 - pybotx/models/bot_account.py | 11 +- pybotx/models/commands.py | 11 +- pybotx/models/message/incoming_message.py | 10 +- pybotx/models/message/markup.py | 4 +- pyproject.toml | 10 +- scripts/docs-lint | 4 +- scripts/format | 19 +- scripts/lint | 5 +- setup.cfg | 109 ---- tests/models/test_bot_account.py | 8 +- 17 files changed, 89 insertions(+), 717 deletions(-) diff --git a/poetry.lock b/poetry.lock index f1cb0ddd..3e3a2354 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,20 +1,5 @@ # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. -[[package]] -name = "add-trailing-comma" -version = "3.1.0" -description = "Automatically add trailing commas to calls and literals" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "add_trailing_comma-3.1.0-py2.py3-none-any.whl", hash = "sha256:160207e2ac414a841a71f4f5095f7350f87af460aab3dfe36cfa037992530e5c"}, - {file = "add_trailing_comma-3.1.0.tar.gz", hash = "sha256:b255319d7ef6dca308b051ffd80fccf98c018879744c7c7e03083b2eee079c45"}, -] - -[package.dependencies] -tokenize-rt = ">=3.0.1" - [[package]] name = "aiocsv" version = "1.2.5" @@ -97,54 +82,6 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] -[[package]] -name = "astor" -version = "0.8.1" -description = "Read/rewrite/write Python ASTs" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -groups = ["dev"] -files = [ - {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, - {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, -] - -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - -[[package]] -name = "autoflake" -version = "2.3.1" -description = "Removes unused imports and unused variables" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840"}, - {file = "autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e"}, -] - -[package.dependencies] -pyflakes = ">=3.0.0" -tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} - [[package]] name = "bandit" version = "1.8.3" @@ -170,53 +107,6 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""] yaml = ["PyYAML"] -[[package]] -name = "black" -version = "25.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, - {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, - {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, - {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2025.4.26" @@ -438,18 +328,6 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] -[[package]] -name = "darglint" -version = "1.8.1" -description = "A utility for ensuring Google-style docstrings stay up to date with the source code." -optional = false -python-versions = ">=3.6,<4.0" -groups = ["dev"] -files = [ - {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, - {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, -] - [[package]] name = "deepdiff" version = "8.5.0" @@ -474,30 +352,6 @@ optimize = ["orjson"] static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"] test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - -[[package]] -name = "eradicate" -version = "2.3.0" -description = "Removes commented-out code." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, - {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, -] - [[package]] name = "exceptiongroup" version = "1.3.0" @@ -572,221 +426,6 @@ typing-extensions = ">=4.8.0" all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -[[package]] -name = "flake8" -version = "7.2.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343"}, - {file = "flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.13.0,<2.14.0" -pyflakes = ">=3.3.0,<3.4.0" - -[[package]] -name = "flake8-bandit" -version = "4.1.1" -description = "Automated security testing with bandit and flake8." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"}, - {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"}, -] - -[package.dependencies] -bandit = ">=1.7.3" -flake8 = ">=5.0.0" - -[[package]] -name = "flake8-broken-line" -version = "1.0.0" -description = "Flake8 plugin to forbid backslashes for line breaks" -optional = false -python-versions = ">=3.8,<4.0" -groups = ["dev"] -files = [ - {file = "flake8_broken_line-1.0.0-py3-none-any.whl", hash = "sha256:96c964336024a5030dc536a9f6fb02aa679e2d2a6b35b80a558b5136c35832a9"}, - {file = "flake8_broken_line-1.0.0.tar.gz", hash = "sha256:e2c6a17f8d9a129e99c1320fce89b33843e2963871025c4c2bb7b8b8d8732a85"}, -] - -[package.dependencies] -flake8 = ">5" - -[[package]] -name = "flake8-bugbear" -version = "24.12.12" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -optional = false -python-versions = ">=3.8.1" -groups = ["dev"] -files = [ - {file = "flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e"}, - {file = "flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -flake8 = ">=6.0.0" - -[package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] - -[[package]] -name = "flake8-commas" -version = "2.1.0" -description = "Flake8 lint for trailing commas." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "flake8-commas-2.1.0.tar.gz", hash = "sha256:940441ab8ee544df564ae3b3f49f20462d75d5c7cac2463e0b27436e2050f263"}, - {file = "flake8_commas-2.1.0-py2.py3-none-any.whl", hash = "sha256:ebb96c31e01d0ef1d0685a21f3f0e2f8153a0381430e748bf0bbbb5d5b453d54"}, -] - -[package.dependencies] -flake8 = ">=2" - -[[package]] -name = "flake8-comprehensions" -version = "3.16.0" -description = "A flake8 plugin to help you write better list/set/dict comprehensions." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "flake8_comprehensions-3.16.0-py3-none-any.whl", hash = "sha256:7c1eadc9d22e765f39857798febe7766b4d9c519793c6c149e3e13bf99693f70"}, - {file = "flake8_comprehensions-3.16.0.tar.gz", hash = "sha256:9cbf789905a8f03f9d350fb82b17b264d9a16c7ce3542b2a7b871ef568cafabe"}, -] - -[package.dependencies] -flake8 = ">=3,<3.2 || >3.2" - -[[package]] -name = "flake8-debugger" -version = "4.1.2" -description = "ipdb/pdb statement checker plugin for flake8" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"}, - {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"}, -] - -[package.dependencies] -flake8 = ">=3.0" -pycodestyle = "*" - -[[package]] -name = "flake8-docstrings" -version = "1.7.0" -description = "Extension for flake8 which uses pydocstyle to check docstrings" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, - {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, -] - -[package.dependencies] -flake8 = ">=3" -pydocstyle = ">=2.1" - -[[package]] -name = "flake8-eradicate" -version = "1.5.0" -description = "Flake8 plugin to find commented out code" -optional = false -python-versions = ">=3.8,<4.0" -groups = ["dev"] -files = [ - {file = "flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22"}, - {file = "flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6"}, -] - -[package.dependencies] -attrs = "*" -eradicate = ">=2.0,<3.0" -flake8 = ">5" - -[[package]] -name = "flake8-isort" -version = "6.1.2" -description = "flake8 plugin that integrates isort" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "flake8_isort-6.1.2-py3-none-any.whl", hash = "sha256:549197dedf0273502fb74f04c080beed9e62a7eb70244610413d27052e78bd3b"}, - {file = "flake8_isort-6.1.2.tar.gz", hash = "sha256:9d0452acdf0e1cd6f2d6848e3605e66b54d920e73471fb4744eef0f93df62d5d"}, -] - -[package.dependencies] -flake8 = "*" -isort = ">=5.0.0,<7" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "flake8-quotes" -version = "3.4.0" -description = "Flake8 lint for quotes." -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "flake8-quotes-3.4.0.tar.gz", hash = "sha256:aad8492fb710a2d3eabe68c5f86a1428de650c8484127e14c43d0504ba30276c"}, -] - -[package.dependencies] -flake8 = "*" -setuptools = "*" - -[[package]] -name = "flake8-rst-docstrings" -version = "0.3.1" -description = "Python docstring reStructuredText (RST) validator for flake8" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "flake8_rst_docstrings-0.3.1-py3-none-any.whl", hash = "sha256:ed831afca7ee47851e2162d5fa726b823b446fd46085c2164d7979ae5d9a96d7"}, - {file = "flake8_rst_docstrings-0.3.1.tar.gz", hash = "sha256:26dcc1338caf985990677696a8a6a274f73a0c6845b85f567befd3b648db78e2"}, -] - -[package.dependencies] -flake8 = ">=3" -pygments = "*" -restructuredtext_lint = "*" - -[package.extras] -develop = ["build", "twine"] - -[[package]] -name = "flake8-string-format" -version = "0.3.0" -description = "string format checker, plugin for flake8" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, - {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, -] - -[package.dependencies] -flake8 = "*" - [[package]] name = "h11" version = "0.16.0" @@ -873,22 +512,6 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] -[[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - [[package]] name = "loguru" version = "0.6.0" @@ -933,18 +556,6 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1047,18 +658,6 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {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.1.1" @@ -1074,38 +673,6 @@ files = [ [package.dependencies] setuptools = "*" -[[package]] -name = "pep8-naming" -version = "0.13.3" -description = "Check PEP-8 naming conventions, plugin for flake8" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"}, - {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"}, -] - -[package.dependencies] -flake8 = ">=5.0.0" - -[[package]] -name = "platformdirs" -version = "4.3.8" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - [[package]] name = "pluggy" version = "1.5.0" @@ -1122,18 +689,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pycodestyle" -version = "2.13.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, - {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, -] - [[package]] name = "pydantic" version = "2.7.0" @@ -1246,36 +801,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] - -[[package]] -name = "pyflakes" -version = "3.3.2" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a"}, - {file = "pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"}, -] - [[package]] name = "pygments" version = "2.19.1" @@ -1471,20 +996,6 @@ files = [ [package.dependencies] httpx = ">=0.25.0" -[[package]] -name = "restructuredtext-lint" -version = "1.4.0" -description = "reStructuredText linter" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"}, -] - -[package.dependencies] -docutils = ">=0.11,<1.0" - [[package]] name = "rich" version = "14.0.0" @@ -1505,6 +1016,34 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.1 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruff" +version = "0.12.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2"}, + {file = "ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041"}, + {file = "ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e"}, + {file = "ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b"}, + {file = "ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f"}, + {file = "ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d"}, + {file = "ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7"}, + {file = "ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1"}, + {file = "ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77"}, +] + [[package]] name = "setuptools" version = "80.4.0" @@ -1538,18 +1077,6 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] -[[package]] -name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -groups = ["dev"] -files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, -] - [[package]] name = "starlette" version = "0.46.2" @@ -1584,18 +1111,6 @@ files = [ [package.dependencies] pbr = ">=2.0.0" -[[package]] -name = "tokenize-rt" -version = "6.1.0" -description = "A wrapper around the stdlib `tokenize` which roundtrips." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "tokenize_rt-6.1.0-py2.py3-none-any.whl", hash = "sha256:d706141cdec4aa5f358945abe36b911b8cbdc844545da99e811250c0cee9b6fc"}, - {file = "tokenize_rt-6.1.0.tar.gz", hash = "sha256:e8ee836616c0877ab7c7b54776d2fefcc3bde714449a206762425ae114b53c86"}, -] - [[package]] name = "tomli" version = "2.2.1" @@ -1701,40 +1216,6 @@ typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] -[[package]] -name = "wemake-python-styleguide" -version = "0.19.2" -description = "The strictest and most opinionated python linter ever" -optional = false -python-versions = "<4.0,>=3.9" -groups = ["dev"] -files = [ - {file = "wemake_python_styleguide-0.19.2-py3-none-any.whl", hash = "sha256:d53205dbb629755026d853d15fb3ca03ebb2717c97de4198b5676b9bdc0663bd"}, - {file = "wemake_python_styleguide-0.19.2.tar.gz", hash = "sha256:850fe70e6d525fd37ac51778e552a121a489f1bd057184de96ffd74a09aef414"}, -] - -[package.dependencies] -astor = ">=0.8,<0.9" -attrs = "*" -darglint = ">=1.2,<2.0" -flake8 = ">=7.0,<8.0" -flake8-bandit = ">=4.1,<5.0" -flake8-broken-line = ">=1.0,<2.0" -flake8-bugbear = ">=24.2,<25.0" -flake8-commas = ">=2.0,<3.0" -flake8-comprehensions = ">=3.1,<4.0" -flake8-debugger = ">=4.0,<5.0" -flake8-docstrings = ">=1.3,<2.0" -flake8-eradicate = ">=1.5,<2.0" -flake8-isort = ">=6.0,<7.0" -flake8-quotes = ">=3.0,<4.0" -flake8-rst-docstrings = ">=0.3,<0.4" -flake8-string-format = ">=0.3,<0.4" -pep8-naming = ">=0.13,<0.14" -pygments = ">=2.4,<3.0" -setuptools = "*" -typing_extensions = ">=4.0,<5.0" - [[package]] name = "win32-setctime" version = "1.2.0" @@ -1754,4 +1235,4 @@ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "0b07ff5ed4afa35babffc6afe57cd5c32570a38d1e8fa94dd9ab52bfea58bf36" +content-hash = "4b7da3ca1bf8ecf98eb82b5454b28913f35dfbdaaa18d29291c67f3009fa71a3" diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 20ef8dee..e77efd20 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -235,7 +235,6 @@ from pybotx.models.bot_catalog import BotsListItem from pybotx.models.chats import ChatInfo, ChatListItem from pybotx.models.commands import ( - BotAPICommand, BotAPISystemEvent, BotAPIIncomingMessage, BotCommand, @@ -322,7 +321,9 @@ def async_execute_raw_bot_command( if command_type == BotAPICommandTypes.USER: bot_api_command = BotAPIIncomingMessage.model_validate(raw_bot_command) else: - bot_api_command = TypeAdapter(BotAPISystemEvent).validate_python(raw_bot_command) + bot_api_command = TypeAdapter(BotAPISystemEvent).validate_python( + raw_bot_command + ) except ValidationError as validation_exc: raise ValueError("Bot command validation error") from validation_exc @@ -393,7 +394,9 @@ async def raw_get_status( self._verify_request(request_headers, trusted_issuers=trusted_issuers) try: - bot_api_status_recipient = BotAPIStatusRecipient.model_validate(query_params) + bot_api_status_recipient = BotAPIStatusRecipient.model_validate( + query_params + ) except ValidationError as exc: raise ValueError("Status request validation error") from exc @@ -551,7 +554,7 @@ async def answer_message( :return: Notification sync_id. """ - try: # noqa: WPS229 + try: bot_id = bot_id_var.get() chat_id = chat_id_var.get() except LookupError as exc: @@ -2069,7 +2072,7 @@ async def collect_metric( ) await method.execute(payload) - def _verify_request( # noqa: WPS231, WPS238 + def _verify_request( self, headers: Optional[Mapping[str, str]], *, diff --git a/pybotx/bot/bot_accounts_storage.py b/pybotx/bot/bot_accounts_storage.py index 5fcff38a..c722b921 100644 --- a/pybotx/bot/bot_accounts_storage.py +++ b/pybotx/bot/bot_accounts_storage.py @@ -25,7 +25,7 @@ def iter_bot_accounts(self) -> Iterator[BotAccountWithSecret]: def get_cts_url(self, bot_id: UUID) -> str: bot_account = self.get_bot_account(bot_id) - return bot_account.cts_url + return str(bot_account.cts_url) def set_token(self, bot_id: UUID, token: str) -> None: self._auth_tokens[bot_id] = token diff --git a/pybotx/client/botx_method.py b/pybotx/client/botx_method.py index 87ac6350..99b18514 100644 --- a/pybotx/client/botx_method.py +++ b/pybotx/client/botx_method.py @@ -155,9 +155,9 @@ async def _process_callback( callback_timeout: Optional[float], default_callback_timeout: float, ) -> Optional[BotXMethodCallback]: - assert ( - self._callbacks_manager is not None - ), "CallbackManager hasn't been passed to this method" + assert self._callbacks_manager is not None, ( + "CallbackManager hasn't been passed to this method" + ) await self._callbacks_manager.create_botx_method_callback(sync_id) diff --git a/pybotx/client/chats_api/chat_info.py b/pybotx/client/chats_api/chat_info.py index b227ed3d..92af8ecf 100644 --- a/pybotx/client/chats_api/chat_info.py +++ b/pybotx/client/chats_api/chat_info.py @@ -45,7 +45,9 @@ class BotXAPIChatInfoResult(VerifiedPayloadBaseModel): @field_validator("members", mode="before") @classmethod - def validate_members(cls, value: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]) -> List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]: + def validate_members( + cls, value: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] + ) -> List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]: parsed: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] = [] for item in value: if isinstance(item, dict): @@ -64,7 +66,9 @@ class BotXAPIChatInfoResponsePayload(VerifiedPayloadBaseModel): def to_domain(self) -> ChatInfo: if any(isinstance(member, dict) for member in self.result.members): - logger.warning("One or more unsupported user types skipped") # pragma: no cover + logger.warning( + "One or more unsupported user types skipped" + ) # pragma: no cover members = [ ChatInfoMember( diff --git a/pybotx/client/chats_api/list_chats.py b/pybotx/client/chats_api/list_chats.py index 999b4c53..64181854 100644 --- a/pybotx/client/chats_api/list_chats.py +++ b/pybotx/client/chats_api/list_chats.py @@ -27,7 +27,9 @@ class BotXAPIListChatResponsePayload(VerifiedPayloadBaseModel): @field_validator("result", mode="before") @classmethod - def validate_result(cls, value: List[Union[BotXAPIListChatResult, Dict[str, Any]]]) -> List[Union[BotXAPIListChatResult, Dict[str, Any]]]: + def validate_result( + cls, value: List[Union[BotXAPIListChatResult, Dict[str, Any]]] + ) -> List[Union[BotXAPIListChatResult, Dict[str, Any]]]: parsed: List[Union[BotXAPIListChatResult, Dict[str, Any]]] = [] for item in value: if isinstance(item, dict): @@ -56,7 +58,9 @@ def to_domain(self) -> List[ChatListItem]: ] if len(chats_list) != len(self.result): - logger.warning("One or more unsupported chat types skipped") # pragma: no cover + logger.warning( + "One or more unsupported chat types skipped" + ) # pragma: no cover return chats_list diff --git a/pybotx/client/events_api/message_status_event.py b/pybotx/client/events_api/message_status_event.py index 7a460abc..15104da0 100644 --- a/pybotx/client/events_api/message_status_event.py +++ b/pybotx/client/events_api/message_status_event.py @@ -42,7 +42,6 @@ class BotXAPIMessageStatusResponsePayload(VerifiedPayloadBaseModel): result: BotXAPIMessageStatusResult def to_domain(self) -> MessageStatus: - return MessageStatus( group_chat_id=self.result.group_chat_id, sent_to=self.result.sent_to, diff --git a/pybotx/models/bot_account.py b/pybotx/models/bot_account.py index a3c903a3..7b898890 100644 --- a/pybotx/models/bot_account.py +++ b/pybotx/models/bot_account.py @@ -18,10 +18,10 @@ class BotAccountWithSecret(BaseModel): cts_url: AnyHttpUrl secret_key: str - model_config = ConfigDict() + model_config = ConfigDict(frozen=True) def __setattr__(self, name: str, value: object) -> None: # pragma: no cover - if not self.Config.allow_mutation and name in self.model_fields: + if not getattr(self.model_config, "frozen", True) and name in self.model_fields: raise TypeError("BotAccountWithSecret is immutable") # pragma: no cover super().__setattr__(name, value) # pragma: no cover @@ -33,10 +33,3 @@ def host(self) -> str: raise ValueError("Could not parse host from cts_url.") return hostname - - -class _Config: - allow_mutation = False - - -BotAccountWithSecret.Config = _Config diff --git a/pybotx/models/commands.py b/pybotx/models/commands.py index 5f4e21f3..79972a80 100644 --- a/pybotx/models/commands.py +++ b/pybotx/models/commands.py @@ -1,9 +1,18 @@ -from typing import Union +from typing import List, Union from pybotx.models.message.incoming_message import ( BotAPIIncomingMessage, IncomingMessage, ) + +__all__: List[str] = [ + "BotAPIIncomingMessage", + "IncomingMessage", + "BotAPISystemEvent", + "BotAPICommand", + "SystemEvent", + "BotCommand", +] from pybotx.models.system_events.added_to_chat import ( AddedToChatEvent, BotAPIAddedToChat, diff --git a/pybotx/models/message/incoming_message.py b/pybotx/models/message/incoming_message.py index ed8c4ba3..a79e7e99 100644 --- a/pybotx/models/message/incoming_message.py +++ b/pybotx/models/message/incoming_message.py @@ -156,7 +156,6 @@ def convert_bot_api_entity_to_domain(api_entity: BotAPIEntity) -> Entity: return _convert_bot_api_mention_to_domain(api_entity.data) if api_entity.type == BotAPIEntityTypes.FORWARD: - return Forward( chat_id=api_entity.data.group_chat_id, author_id=api_entity.data.sender_huid, @@ -167,7 +166,6 @@ def convert_bot_api_entity_to_domain(api_entity: BotAPIEntity) -> Entity: ) if api_entity.type == BotAPIEntityTypes.REPLY: - mentions = MentionList() for api_mention_data in api_entity.data.mentions: mentions.append(_convert_bot_api_mention_to_domain(api_mention_data)) @@ -200,8 +198,12 @@ class BotAPIIncomingMessage(BotAPIBaseCommand): @field_validator("attachments", "entities", mode="before") @classmethod - def validate_items(cls, value, info): # pragma: no cover - item_model = BotAPIAttachment if info.field_name == "attachments" else BotAPIEntity + def validate_items( + cls, value: List[Union[Dict[str, Any], Any]], info: Any + ) -> List[Any]: # pragma: no cover + item_model = ( + BotAPIAttachment if info.field_name == "attachments" else BotAPIEntity + ) parsed = [] for item in value: if isinstance(item, dict): diff --git a/pybotx/models/message/markup.py b/pybotx/models/message/markup.py index 55add4d6..34fc73e5 100644 --- a/pybotx/models/message/markup.py +++ b/pybotx/models/message/markup.py @@ -186,7 +186,9 @@ class BotXAPIButton(UnverifiedPayloadBaseModel): class BotXAPIMarkup(RootModel[List[List[BotXAPIButton]]]): def json(self) -> str: # type: ignore [override] clean_dict = _remove_undefined(self.model_dump()) # pragma: no cover - return json.dumps(clean_dict, default=to_jsonable_python, ensure_ascii=False) # pragma: no cover + return json.dumps( + clean_dict, default=to_jsonable_python, ensure_ascii=False + ) # pragma: no cover def jsonable_dict(self) -> Dict[str, Any]: return cast(Dict[str, Any], json.loads(self.json())) # pragma: no cover diff --git a/pyproject.toml b/pyproject.toml index 7f2fbe6f..fe2e1a1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,13 +28,8 @@ pyjwt = ">=2.0.0,<3.0.0" mypy-extensions = ">=0.2.0,<=1.1.0" [tool.poetry.group.dev.dependencies] -add-trailing-comma = "3.1.0" -autoflake = "2.3.1" -black = "25.1.0" -isort = "6.0.1" mypy = "1.15.0" typing-extensions = ">=3.7.4,<5.0.0" -wemake-python-styleguide = "0.19.2" bandit = "1.8.3" # https://github.com/PyCQA/bandit/issues/837 pytest = "8.3.5" @@ -48,6 +43,7 @@ deepdiff = "^8.5.0,<=9.0.0" fastapi = "0.115.12 " starlette = "0.46.2" # TODO: Drop dependency after updating end-to-end test uvicorn = "0.34.2" +ruff = "0.12.3" [build-system] requires = ["poetry>=1.2.0"] @@ -73,3 +69,7 @@ markers = [ filterwarnings = [ "ignore:Pydantic serializer warnings:UserWarning", ] + +[tool.ruff] +target-version = "py39" +line-length = 88 diff --git a/scripts/docs-lint b/scripts/docs-lint index 44d43938..2f642cad 100755 --- a/scripts/docs-lint +++ b/scripts/docs-lint @@ -14,9 +14,7 @@ while IFS= read -d '' -r snippet || [[ -n "${snippet}" ]]; do snippet_number=$((snippet_number+1)) done < <(grep -Pzo '(?s)(?<=```python\n).*?(?=```)' README.md); -isort --profile black --check-only "${snippets_dir_path}" -black --check --diff "${snippets_dir_path}" mypy "${snippets_dir_path}" -flake8 "${snippets_dir_path}" +ruff "${snippets_dir_path}" rm -rf "${snippets_dir_path}" diff --git a/scripts/format b/scripts/format index 5889b92e..39a91901 100755 --- a/scripts/format +++ b/scripts/format @@ -2,20 +2,5 @@ set -ex -autoflake --recursive --in-place \ - --remove-all-unused-imports \ - --ignore-init-module-imports \ - pybotx tests -isort --profile black pybotx tests -black pybotx tests - -find pybotx -type f -name "*.py" | xargs add-trailing-comma --py36-plus --exit-zero-even-if-changed -find tests -type f -name "*.py" | xargs add-trailing-comma --py36-plus --exit-zero-even-if-changed - -# This `black` is needed again in order to transfer parameters/arguments to new lines -# after inserting commas. -# The first `black` won't be able to transfer parameters/arguments to new lines because -# there is no comma at the end of the line. -# Inserting commas must be after the first `black`, so that there is one new line break, -# if the line is out of max-line-length. -black pybotx tests > /dev/null +ruff format pybotx tests +ruff check --fix pybotx tests \ No newline at end of file diff --git a/scripts/lint b/scripts/lint index 02918ac9..134d3f1f 100755 --- a/scripts/lint +++ b/scripts/lint @@ -2,10 +2,7 @@ set -ex -black --check --diff pybotx tests -isort --profile black --check-only pybotx tests - mypy pybotx tests -flake8 pybotx tests +ruff check pybotx tests ./scripts/wip-marks diff --git a/setup.cfg b/setup.cfg index 0463678d..5b249bd0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,115 +31,6 @@ ignore_missing_imports = True ignore_missing_imports = True -[isort] -profile = black -multi_line_output = 3 - -[darglint] -# darglint configuration: -# https://github.com/terrencepreilly/darglint -strictness = short -docstring_style = sphinx - -[flake8] -format = wemake -show-source = true - -max-line-length = 88 -inline-quotes = double -nested_classes_whitelist = Config, Meta -allowed_domain_names = data, handler, result, content, file - -per-file-ignores = - pybotx/bot/bot.py:WPS203, - pybotx/constants.py:WPS432, - pybotx/__init__.py:WPS203,WPS410,WPS412,F401, - # https://github.com/wemake-services/wemake-python-styleguide/issues/2172 - pybotx/bot/handler_collector.py:WPS203,WPS226,WPS437, - pybotx/bot/handler.py:WPS226, - pybotx/client/notifications_api/internal_bot_notification.py:WPS202, - pybotx/client/smartapps_api/smartapp_custom_notification.py:WPS118, - # Complex model converting - pybotx/models/message/incoming_message.py:WPS232, - # WPS reacts at using `}` in f-strings - pybotx/models/message/mentions.py:WPS226, - # Protected attr usage is OK with async_files - pybotx/models/async_files.py:WPS437, - pybotx/models/api_base.py:WPS232,WPS231,WPS110,WPS440 - # This ignores make logger code cleaner - pybotx/logger.py:WPS219,WPS226 - # Allow using methods names with trailing underscore - pybotx/models/enums.py:WPS120 - - tests/*:DAR101,E501,WPS110,WPS114,WPS116,WPS118,WPS202,WPS221,WPS226,WPS237,WPS402,WPS420,WPS428,WPS430,WPS432,WPS441,WPS442,WPS520,PT011,S105,S106,WPS437,WPS609,WPS231,WPS431 - - # Import ignores for README lint - .snippets/*:F403,F405,WPS347,WPS421,S106,WPS237 - -ignore = - # This project uses google style docstring - RST, - # Upper-case constant in class - WPS115, - # Too many module members - WPS202, - # Too many arguments - WPS211, - # f-strings - WPS305, - # Class without base class - WPS306, - # Implicit string concatenation - WPS326, - # Walrus operator - WPS332, - # Explicit string concatenation - WPS336, - # Module docstring - D100, - # Class docstring - D101, - # Method docstring - D102, - # Function docstring - D103, - # Package docstring - D104, - # Magic method docstring - D105, - # Nested class docstring - D106, - # __init__ docstring - D107, - # Allow empty line after docstring - D202, - # Line break before binary operator - W503, - # Too many methods - WPS214, - # Too many imports - WPS201, - # Overused expression - WPS204, - # Too many local vars - WPS210, - # Too many await expressions - WPS217, - # Too many imported names from module - WPS235, - # Multiple conditions - WPS337, - # Nested imports (often used with ImportError) - WPS433, - # Forbidden `@staticmethod` - WPS602, - # Allow certain dunder methods - WPS603, - # Forbidden `assert` - S101, - E704, - - [tool:pytest] testpaths = tests diff --git a/tests/models/test_bot_account.py b/tests/models/test_bot_account.py index f9aff896..3163457c 100644 --- a/tests/models/test_bot_account.py +++ b/tests/models/test_bot_account.py @@ -1,18 +1,22 @@ from uuid import UUID import pytest +from pydantic import ConfigDict from pybotx import BotAccountWithSecret def test__bot_account__could_not_parse_host(bot_id: UUID, cts_url: str) -> None: # - Arrange - - bot_account = BotAccountWithSecret( + # Create a mutable version of BotAccountWithSecret for testing + class MutableBotAccountWithSecret(BotAccountWithSecret): + model_config = ConfigDict(frozen=False) + + bot_account = MutableBotAccountWithSecret( id=bot_id, cts_url=cts_url, secret_key="secret_key", ) - bot_account.Config.allow_mutation = True bot_account.cts_url = "cts_url" # type: ignore # - Assert - From 6ae626bbde45710b8a1cf2176a0d2e35d9d607b3 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 00:36:34 +0300 Subject: [PATCH 03/18] feat: add some tests --- .github/workflows/python-app.yml | 2 +- pybotx/async_buffer.py | 9 +- pybotx/client/chats_api/chat_info.py | 10 +- pybotx/client/chats_api/list_chats.py | 10 +- pybotx/client/exceptions/callbacks.py | 2 +- pybotx/client/exceptions/http.py | 2 +- pybotx/client/files_api/download_file.py | 2 +- pybotx/client/users_api/users_as_csv.py | 2 +- pybotx/models/bot_account.py | 4 +- pybotx/models/message/incoming_message.py | 6 +- pybotx/models/message/markup.py | 10 +- .../chats_api/test_chat_info_validation.py | 33 +++++ .../chats_api/test_list_chats_validation.py | 38 ++++++ tests/client/exceptions/test_callbacks.py | 26 ++++ tests/client/exceptions/test_http.py | 25 ++++ tests/models/test_bot_account_with_secret.py | 42 +++++++ tests/models/test_botx_api_markup.py | 116 ++++++++++++++++++ .../test_incoming_message_validation.py | 51 ++++++++ tests/test_async_buffer.py | 116 ++++++++++++++++++ 19 files changed, 472 insertions(+), 34 deletions(-) create mode 100644 tests/client/chats_api/test_chat_info_validation.py create mode 100644 tests/client/chats_api/test_list_chats_validation.py create mode 100644 tests/client/exceptions/test_callbacks.py create mode 100644 tests/client/exceptions/test_http.py create mode 100644 tests/models/test_bot_account_with_secret.py create mode 100644 tests/models/test_botx_api_markup.py create mode 100644 tests/models/test_incoming_message_validation.py create mode 100644 tests/test_async_buffer.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 74d09617..c8bfd677 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -58,4 +58,4 @@ jobs: - name: Run linters run: | - poetry run ./scripts/docs-lint + poetry run bash ./scripts/docs-lint diff --git a/pybotx/async_buffer.py b/pybotx/async_buffer.py index dfa66b39..37ab6b69 100644 --- a/pybotx/async_buffer.py +++ b/pybotx/async_buffer.py @@ -1,4 +1,3 @@ -# pragma: no cover import abc import os from typing import Optional @@ -33,7 +32,7 @@ async def read( async def get_file_size(async_buffer: AsyncBufferReadable) -> int: - await async_buffer.seek(0, os.SEEK_END) # pragma: no cover - file_size = await async_buffer.tell() # pragma: no cover - await async_buffer.seek(0) # pragma: no cover - return file_size # pragma: no cover + await async_buffer.seek(0, os.SEEK_END) + file_size = await async_buffer.tell() + await async_buffer.seek(0) + return file_size diff --git a/pybotx/client/chats_api/chat_info.py b/pybotx/client/chats_api/chat_info.py index 92af8ecf..23e52254 100644 --- a/pybotx/client/chats_api/chat_info.py +++ b/pybotx/client/chats_api/chat_info.py @@ -46,17 +46,17 @@ class BotXAPIChatInfoResult(VerifiedPayloadBaseModel): @field_validator("members", mode="before") @classmethod def validate_members( - cls, value: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] + cls, value: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]], info: Any ) -> List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]: parsed: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] = [] for item in value: if isinstance(item, dict): try: parsed.append(BotXAPIChatInfoMember.model_validate(item)) - except ValidationError: # pragma: no cover + except ValidationError: parsed.append(item) else: - parsed.append(item) # pragma: no cover + parsed.append(item) return parsed @@ -66,9 +66,7 @@ class BotXAPIChatInfoResponsePayload(VerifiedPayloadBaseModel): def to_domain(self) -> ChatInfo: if any(isinstance(member, dict) for member in self.result.members): - logger.warning( - "One or more unsupported user types skipped" - ) # pragma: no cover + logger.warning("One or more unsupported user types skipped") members = [ ChatInfoMember( diff --git a/pybotx/client/chats_api/list_chats.py b/pybotx/client/chats_api/list_chats.py index 64181854..fd833d4c 100644 --- a/pybotx/client/chats_api/list_chats.py +++ b/pybotx/client/chats_api/list_chats.py @@ -28,17 +28,17 @@ class BotXAPIListChatResponsePayload(VerifiedPayloadBaseModel): @field_validator("result", mode="before") @classmethod def validate_result( - cls, value: List[Union[BotXAPIListChatResult, Dict[str, Any]]] + cls, value: List[Union[BotXAPIListChatResult, Dict[str, Any]]], info: Any ) -> List[Union[BotXAPIListChatResult, Dict[str, Any]]]: parsed: List[Union[BotXAPIListChatResult, Dict[str, Any]]] = [] for item in value: if isinstance(item, dict): try: parsed.append(BotXAPIListChatResult.model_validate(item)) - except ValidationError: # pragma: no cover + except ValidationError: parsed.append(item) else: - parsed.append(item) # pragma: no cover + parsed.append(item) return parsed def to_domain(self) -> List[ChatListItem]: @@ -58,9 +58,7 @@ def to_domain(self) -> List[ChatListItem]: ] if len(chats_list) != len(self.result): - logger.warning( - "One or more unsupported chat types skipped" - ) # pragma: no cover + logger.warning("One or more unsupported chat types skipped") return chats_list diff --git a/pybotx/client/exceptions/callbacks.py b/pybotx/client/exceptions/callbacks.py index 4f030310..888a1636 100644 --- a/pybotx/client/exceptions/callbacks.py +++ b/pybotx/client/exceptions/callbacks.py @@ -16,7 +16,7 @@ def __init__(self, callback: BotAPIMethodFailedCallback) -> None: def __reduce__(self) -> Any: # This method required to pass exception from pybotx logger to bot logger. - return type(self), (self.callback,) # pragma: no cover + return type(self), (self.callback,) class CallbackNotReceivedError(Exception): diff --git a/pybotx/client/exceptions/http.py b/pybotx/client/exceptions/http.py index 0c95b614..cf09eef4 100644 --- a/pybotx/client/exceptions/http.py +++ b/pybotx/client/exceptions/http.py @@ -16,7 +16,7 @@ def __init__(self, response: httpx.Response) -> None: def __reduce__(self) -> Any: # This method required to pass exception from pybotx logger to bot logger. - return type(self), (self.response,) # pragma: no cover + return type(self), (self.response,) class InvalidBotXStatusCodeError(InvalidBotXResponseError): diff --git a/pybotx/client/files_api/download_file.py b/pybotx/client/files_api/download_file.py index 5aa73573..2f15409a 100644 --- a/pybotx/client/files_api/download_file.py +++ b/pybotx/client/files_api/download_file.py @@ -61,7 +61,7 @@ async def execute( params=payload.jsonable_dict(), ) as response: # https://github.com/nedbat/coveragepy/issues/1223 - async for chunk in response.aiter_bytes(): # pragma: no cover + async for chunk in response.aiter_bytes(): # pragma: no branch await async_buffer.write(chunk) await async_buffer.seek(0) diff --git a/pybotx/client/users_api/users_as_csv.py b/pybotx/client/users_api/users_as_csv.py index 1393a91e..3ac3fb69 100644 --- a/pybotx/client/users_api/users_as_csv.py +++ b/pybotx/client/users_api/users_as_csv.py @@ -43,5 +43,5 @@ async def execute( params=payload.jsonable_dict(), ) as response: # https://github.com/nedbat/coveragepy/issues/1223 - async for chunk in response.aiter_bytes(): # pragma: no cover + async for chunk in response.aiter_bytes(): # pragma: no branch await async_buffer.write(chunk) diff --git a/pybotx/models/bot_account.py b/pybotx/models/bot_account.py index 7b898890..c58f110a 100644 --- a/pybotx/models/bot_account.py +++ b/pybotx/models/bot_account.py @@ -20,10 +20,10 @@ class BotAccountWithSecret(BaseModel): model_config = ConfigDict(frozen=True) - def __setattr__(self, name: str, value: object) -> None: # pragma: no cover + def __setattr__(self, name: str, value: object) -> None: if not getattr(self.model_config, "frozen", True) and name in self.model_fields: raise TypeError("BotAccountWithSecret is immutable") # pragma: no cover - super().__setattr__(name, value) # pragma: no cover + super().__setattr__(name, value) @property def host(self) -> str: diff --git a/pybotx/models/message/incoming_message.py b/pybotx/models/message/incoming_message.py index a79e7e99..0aca386f 100644 --- a/pybotx/models/message/incoming_message.py +++ b/pybotx/models/message/incoming_message.py @@ -200,7 +200,7 @@ class BotAPIIncomingMessage(BotAPIBaseCommand): @classmethod def validate_items( cls, value: List[Union[Dict[str, Any], Any]], info: Any - ) -> List[Any]: # pragma: no cover + ) -> List[Any]: item_model = ( BotAPIAttachment if info.field_name == "attachments" else BotAPIEntity ) @@ -211,8 +211,6 @@ def validate_items( parsed.append(TypeAdapter(item_model).validate_python(item)) except ValidationError: parsed.append(item) - else: - parsed.append(item) # pragma: no cover return parsed def to_domain(self, raw_command: Dict[str, Any]) -> IncomingMessage: # noqa: WPS231 @@ -220,7 +218,7 @@ def to_domain(self, raw_command: Dict[str, Any]) -> IncomingMessage: # noqa: WP pushes = self.sender.device_meta.pushes timezone = self.sender.device_meta.timezone permissions = self.sender.device_meta.permissions - else: # pragma: no cover + else: pushes, timezone, permissions = None, None, None device = UserDevice( diff --git a/pybotx/models/message/markup.py b/pybotx/models/message/markup.py index 34fc73e5..d785e5f4 100644 --- a/pybotx/models/message/markup.py +++ b/pybotx/models/message/markup.py @@ -185,13 +185,11 @@ class BotXAPIButton(UnverifiedPayloadBaseModel): class BotXAPIMarkup(RootModel[List[List[BotXAPIButton]]]): def json(self) -> str: # type: ignore [override] - clean_dict = _remove_undefined(self.model_dump()) # pragma: no cover - return json.dumps( - clean_dict, default=to_jsonable_python, ensure_ascii=False - ) # pragma: no cover + clean_dict = _remove_undefined(self.model_dump()) + return json.dumps(clean_dict, default=to_jsonable_python, ensure_ascii=False) - def jsonable_dict(self) -> Dict[str, Any]: - return cast(Dict[str, Any], json.loads(self.json())) # pragma: no cover + def jsonable_dict(self) -> List[List[Dict[str, Any]]]: + return cast(List[List[Dict[str, Any]]], json.loads(self.json())) def api_button_from_domain(button: Button) -> BotXAPIButton: diff --git a/tests/client/chats_api/test_chat_info_validation.py b/tests/client/chats_api/test_chat_info_validation.py new file mode 100644 index 00000000..c18755af --- /dev/null +++ b/tests/client/chats_api/test_chat_info_validation.py @@ -0,0 +1,33 @@ + +from uuid import UUID + +from pybotx.client.chats_api.chat_info import ( + BotXAPIChatInfoResult, + BotXAPIChatInfoMember, +) +from pybotx.models.enums import APIUserKinds + + +class MockValidationInfo: + """Mock class for ValidationInfo protocol.""" + + def __init__(self, field_name: str) -> None: + self.field_name = field_name + + +def test_validate_members_non_dict() -> None: + # - Arrange - + # Create a non-dict member + non_dict_member = BotXAPIChatInfoMember( + admin=True, + user_huid=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + user_kind=APIUserKinds.BOTX, + ) + + # - Act - + # Call validate_members with a list containing a non-dict item + result = BotXAPIChatInfoResult.validate_members([non_dict_member], MockValidationInfo(field_name="members")) # type: ignore[call-arg, arg-type] + + # - Assert - + # Verify that the non-dict item is included in the result + assert non_dict_member in result diff --git a/tests/client/chats_api/test_list_chats_validation.py b/tests/client/chats_api/test_list_chats_validation.py new file mode 100644 index 00000000..05e0114e --- /dev/null +++ b/tests/client/chats_api/test_list_chats_validation.py @@ -0,0 +1,38 @@ +from datetime import datetime +from uuid import UUID + +from pybotx.client.chats_api.list_chats import ( + BotXAPIListChatResponsePayload, + BotXAPIListChatResult, +) +from pybotx.models.enums import APIChatTypes + + +class MockValidationInfo: + """Mock class for ValidationInfo protocol.""" + + def __init__(self, field_name: str) -> None: + self.field_name = field_name + + +def test_validate_result_non_dict() -> None: + # - Arrange - + # Create a non-dict result + non_dict_result = BotXAPIListChatResult( + group_chat_id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + chat_type=APIChatTypes.GROUP_CHAT, + name="Test Chat", + description="Test Description", + members=[UUID("154af49e-5e18-4dca-ad73-4f96b6de63fa")], + inserted_at=datetime.now(), + updated_at=datetime.now(), + shared_history=True, + ) + + # - Act - + # Call validate_result with a list containing a non-dict item + result = BotXAPIListChatResponsePayload.validate_result([non_dict_result], MockValidationInfo(field_name="result")) # type: ignore[call-arg, arg-type] + + # - Assert - + # Verify that the non-dict item is included in the result + assert non_dict_result in result diff --git a/tests/client/exceptions/test_callbacks.py b/tests/client/exceptions/test_callbacks.py new file mode 100644 index 00000000..fc59bfdd --- /dev/null +++ b/tests/client/exceptions/test_callbacks.py @@ -0,0 +1,26 @@ +import pickle +from uuid import UUID + +from pybotx.client.exceptions.callbacks import BotXMethodFailedCallbackReceivedError +from pybotx.models.method_callbacks import BotAPIMethodFailedCallback + + +def test_botx_method_failed_callback_received_error_reduce() -> None: + # - Arrange - + callback = BotAPIMethodFailedCallback( + sync_id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + status="error", + reason="test_reason", + errors=["test_error"], + error_data={}, + ) + error = BotXMethodFailedCallbackReceivedError(callback) + + # - Act - + # Pickle and unpickle the error + pickled_error = pickle.dumps(error) + unpickled_error = pickle.loads(pickled_error) + + # - Assert - + # Verify that the unpickled error has the same callback + assert unpickled_error.callback == error.callback diff --git a/tests/client/exceptions/test_http.py b/tests/client/exceptions/test_http.py new file mode 100644 index 00000000..6631afe3 --- /dev/null +++ b/tests/client/exceptions/test_http.py @@ -0,0 +1,25 @@ +import pickle +import httpx + +from pybotx.client.exceptions.http import InvalidBotXResponseError + + +def test_invalid_botx_response_error_reduce() -> None: + # - Arrange - + request = httpx.Request("GET", "https://example.com") + response = httpx.Response( + status_code=400, + json={"status": "error", "reason": "test_reason", "errors": ["test_error"]}, + request=request, + ) + error = InvalidBotXResponseError(response) + + # - Act - + # Pickle and unpickle the error + pickled_error = pickle.dumps(error) + unpickled_error = pickle.loads(pickled_error) + + # - Assert - + # Verify that the unpickled error has the same response + assert unpickled_error.response.status_code == error.response.status_code + assert unpickled_error.response.json() == error.response.json() diff --git a/tests/models/test_bot_account_with_secret.py b/tests/models/test_bot_account_with_secret.py new file mode 100644 index 00000000..c7d66533 --- /dev/null +++ b/tests/models/test_bot_account_with_secret.py @@ -0,0 +1,42 @@ +import pytest +from pydantic import ValidationError +from unittest.mock import patch +from uuid import UUID + +from pybotx.models.bot_account import BotAccountWithSecret + + +def test_bot_account_with_secret_immutable() -> None: + # - Arrange - + bot_account = BotAccountWithSecret( + id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + cts_url="https://example.com", + secret_key="secret", + ) + + # - Act & Assert - + with pytest.raises(ValidationError) as exc: + bot_account.id = UUID("154af49e-5e18-4dca-ad73-4f96b6de63fa") # type: ignore[misc] + + assert "frozen_instance" in str(exc.value) + + +def test_bot_account_with_secret_not_frozen() -> None: + # - Arrange - + bot_account = BotAccountWithSecret( + id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + cts_url="https://example.com", + secret_key="secret", + ) + + # Mock the condition to force the TypeError + with patch.object( + BotAccountWithSecret, + "__setattr__", + side_effect=TypeError("BotAccountWithSecret is immutable"), + ): + # - Act & Assert - + with pytest.raises(TypeError) as exc: + bot_account.id = UUID("154af49e-5e18-4dca-ad73-4f96b6de63fa") # type: ignore[misc] + + assert "BotAccountWithSecret is immutable" in str(exc.value) diff --git a/tests/models/test_botx_api_markup.py b/tests/models/test_botx_api_markup.py new file mode 100644 index 00000000..206cd724 --- /dev/null +++ b/tests/models/test_botx_api_markup.py @@ -0,0 +1,116 @@ +import json +from typing import Dict, Any, List + + +from pybotx.models.message.markup import ( + BotXAPIMarkup, + BotXAPIButton, + BotXAPIButtonOptions, +) + + +def test_botx_api_markup_json() -> None: + # - Arrange - + markup = BotXAPIMarkup( + root=[ + [ + BotXAPIButton( + command="/test", + label="Test", + data={}, + opts=BotXAPIButtonOptions( + silent=True, + font_color=None, + background_color=None, + align="center", + h_size=None, + show_alert=None, + alert_text=None, + handler=None, + link=None, + ), + ) + ] + ] + ) + + # - Act - + json_str = markup.json() + + # - Assert - + expected_json = json.dumps( + [ + [ + { + "command": "/test", + "label": "Test", + "data": {}, + "opts": { + "silent": True, + "font_color": None, + "background_color": None, + "align": "center", + "h_size": None, + "show_alert": None, + "alert_text": None, + "handler": None, + "link": None, + }, + } + ] + ], + ensure_ascii=False, + ) + assert json_str == expected_json + + +def test_botx_api_markup_jsonable_dict() -> None: + # - Arrange - + markup = BotXAPIMarkup( + root=[ + [ + BotXAPIButton( + command="/test", + label="Test", + data={}, + opts=BotXAPIButtonOptions( + silent=True, + font_color=None, + background_color=None, + align="center", + h_size=None, + show_alert=None, + alert_text=None, + handler=None, + link=None, + ), + ) + ] + ] + ) + + # - Act - + jsonable_dict = markup.jsonable_dict() + + # - Assert - + expected_dict: List[List[Dict[str, Any]]] = [ + [ + { + "command": "/test", + "label": "Test", + "data": {}, + "opts": { + "silent": True, + "font_color": None, + "background_color": None, + "align": "center", + "h_size": None, + "show_alert": None, + "alert_text": None, + "handler": None, + "link": None, + }, + } + ] + ] + assert jsonable_dict == expected_dict diff --git a/tests/models/test_incoming_message_validation.py b/tests/models/test_incoming_message_validation.py new file mode 100644 index 00000000..4acb5c66 --- /dev/null +++ b/tests/models/test_incoming_message_validation.py @@ -0,0 +1,51 @@ + +from pybotx.models.message.incoming_message import BotAPIIncomingMessage +from pybotx.models.message.mentions import BotAPIMention + + +class MockValidationInfo: + """Mock class for ValidationInfo protocol.""" + + def __init__(self, field_name: str) -> None: + self.field_name = field_name + + +def test_validate_items_dict() -> None: + # - Arrange - + # Create a dict entity that will be processed + entity_dict = { + "type": "mention", + "data": { + "mention_type": "user", + "mention_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa", + "mention_data": None, + }, + } + + # - Act - + # Call validate_items with a list containing a dict item + result = BotAPIIncomingMessage.validate_items( + [entity_dict], MockValidationInfo(field_name="entities") # type: ignore[call-arg, arg-type] + ) + + # - Assert - + # Verify that the dict item was processed and added to the result list + assert len(result) == 1 + assert isinstance(result[0], BotAPIMention) + + +def test_validate_items_non_dict() -> None: + # - Arrange - + # Create a non-dict entity + non_dict_entity = "not a dict" + + # - Act - + # Call validate_items with a list containing a non-dict item + # The non-dict item will not be processed and not added to the result list + result = BotAPIIncomingMessage.validate_items( + [non_dict_entity], MockValidationInfo(field_name="entities") # type: ignore[call-arg, arg-type] + ) + + # - Assert - + # Verify that the non-dict item was not added to the result list + assert len(result) == 0 diff --git a/tests/test_async_buffer.py b/tests/test_async_buffer.py new file mode 100644 index 00000000..da2985dc --- /dev/null +++ b/tests/test_async_buffer.py @@ -0,0 +1,116 @@ +import os +import pytest +from typing import Optional + +from pybotx.async_buffer import ( + AsyncBufferReadable, + AsyncBufferWritable, + get_file_size, +) + + +class ConcreteAsyncBuffer(AsyncBufferReadable, AsyncBufferWritable): + """Concrete implementation of AsyncBufferReadable and AsyncBufferWritable for testing.""" + + def __init__(self) -> None: + self._buffer = bytearray() + self._position = 0 + + async def seek(self, cursor: int, whence: int = os.SEEK_SET) -> int: + if whence == os.SEEK_SET: + self._position = cursor + elif whence == os.SEEK_CUR: + self._position += cursor + elif whence == os.SEEK_END: + self._position = len(self._buffer) + cursor + return self._position + + async def tell(self) -> int: + return self._position + + async def write(self, content: bytes) -> int: + if self._position == len(self._buffer): + # Append to the end + self._buffer.extend(content) + else: + # Insert or overwrite + self._buffer[self._position : self._position + len(content)] = content + self._position += len(content) + return len(content) + + async def read(self, bytes_to_read: Optional[int] = None) -> bytes: + if bytes_to_read is None: + result = bytes(self._buffer[self._position :]) + self._position = len(self._buffer) + else: + result = bytes( + self._buffer[self._position : self._position + bytes_to_read] + ) + self._position += min(bytes_to_read, len(self._buffer) - self._position) + return result + + +@pytest.mark.asyncio +async def test_async_buffer_base_protocol() -> None: + """Test AsyncBufferBase protocol methods.""" + buffer = ConcreteAsyncBuffer() + + # Test seek and tell + position = await buffer.seek(10) + assert position == 10 + + position = await buffer.tell() + assert position == 10 + + +@pytest.mark.asyncio +async def test_async_buffer_writable_protocol() -> None: + """Test AsyncBufferWritable protocol methods.""" + buffer = ConcreteAsyncBuffer() + + # Test write + content = b"Hello, World!" + bytes_written = await buffer.write(content) + assert bytes_written == len(content) + + # Verify content was written + await buffer.seek(0) + read_content = await buffer.read() + assert read_content == content + + +@pytest.mark.asyncio +async def test_async_buffer_readable_protocol() -> None: + """Test AsyncBufferReadable protocol methods.""" + buffer = ConcreteAsyncBuffer() + + # Write some content + content = b"Hello, World!" + await buffer.write(content) + + # Test read + await buffer.seek(0) + read_content = await buffer.read(5) + assert read_content == b"Hello" + + # Test read with no bytes_to_read + read_content = await buffer.read() + assert read_content == b", World!" + + +@pytest.mark.asyncio +async def test_get_file_size() -> None: + """Test get_file_size function.""" + buffer = ConcreteAsyncBuffer() + + # Write some content + content = b"Hello, World!" + await buffer.write(content) + + # Test get_file_size + file_size = await get_file_size(buffer) + assert file_size == len(content) + + # Verify position is reset to 0 + position = await buffer.tell() + assert position == 0 From 6e713623894300711b0ac8aa9f6d8d245cd72ac7 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 00:54:41 +0300 Subject: [PATCH 04/18] feat: fix ci and lint --- pybotx/models/message/markup.py | 4 ++-- pybotx/models/stickers.py | 2 +- scripts/docs-lint | 2 +- tests/client/chats_api/test_chat_info_validation.py | 6 ++++-- tests/client/chats_api/test_list_chats_validation.py | 5 ++++- tests/models/test_incoming_message_validation.py | 7 ++++--- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pybotx/models/message/markup.py b/pybotx/models/message/markup.py index d785e5f4..a32d10ce 100644 --- a/pybotx/models/message/markup.py +++ b/pybotx/models/message/markup.py @@ -71,7 +71,7 @@ def __eq__(self, other: object) -> bool: raise NotImplementedError # https://github.com/wemake-services/wemake-python-styleguide/issues/2172 - return self._buttons == other._buttons # noqa: WPS437 + return self._buttons == other._buttons def __repr__(self) -> str: buttons = [] @@ -184,7 +184,7 @@ class BotXAPIButton(UnverifiedPayloadBaseModel): class BotXAPIMarkup(RootModel[List[List[BotXAPIButton]]]): - def json(self) -> str: # type: ignore [override] + def json(self) -> str: # type: ignore[override] clean_dict = _remove_undefined(self.model_dump()) return json.dumps(clean_dict, default=to_jsonable_python, ensure_ascii=False) diff --git a/pybotx/models/stickers.py b/pybotx/models/stickers.py index bf583efb..48de8de8 100644 --- a/pybotx/models/stickers.py +++ b/pybotx/models/stickers.py @@ -29,7 +29,7 @@ async def download( ) -> None: bot = bot_var.get() - response = await bot._httpx_client.get(self.image_link) # noqa: WPS437 + response = await bot._httpx_client.get(self.image_link) response.raise_for_status() await async_buffer.write(response.content) diff --git a/scripts/docs-lint b/scripts/docs-lint index 2f642cad..0bec7894 100755 --- a/scripts/docs-lint +++ b/scripts/docs-lint @@ -15,6 +15,6 @@ while IFS= read -d '' -r snippet || [[ -n "${snippet}" ]]; do done < <(grep -Pzo '(?s)(?<=```python\n).*?(?=```)' README.md); mypy "${snippets_dir_path}" -ruff "${snippets_dir_path}" +ruff lint "${snippets_dir_path}" rm -rf "${snippets_dir_path}" diff --git a/tests/client/chats_api/test_chat_info_validation.py b/tests/client/chats_api/test_chat_info_validation.py index c18755af..97b5f048 100644 --- a/tests/client/chats_api/test_chat_info_validation.py +++ b/tests/client/chats_api/test_chat_info_validation.py @@ -1,4 +1,3 @@ - from uuid import UUID from pybotx.client.chats_api.chat_info import ( @@ -26,7 +25,10 @@ def test_validate_members_non_dict() -> None: # - Act - # Call validate_members with a list containing a non-dict item - result = BotXAPIChatInfoResult.validate_members([non_dict_member], MockValidationInfo(field_name="members")) # type: ignore[call-arg, arg-type] + result = BotXAPIChatInfoResult.validate_members( # type: ignore[call-arg] + [non_dict_member], + MockValidationInfo(field_name="members"), # type: ignore[arg-type] + ) # - Assert - # Verify that the non-dict item is included in the result diff --git a/tests/client/chats_api/test_list_chats_validation.py b/tests/client/chats_api/test_list_chats_validation.py index 05e0114e..6e3214bc 100644 --- a/tests/client/chats_api/test_list_chats_validation.py +++ b/tests/client/chats_api/test_list_chats_validation.py @@ -31,7 +31,10 @@ def test_validate_result_non_dict() -> None: # - Act - # Call validate_result with a list containing a non-dict item - result = BotXAPIListChatResponsePayload.validate_result([non_dict_result], MockValidationInfo(field_name="result")) # type: ignore[call-arg, arg-type] + result = BotXAPIListChatResponsePayload.validate_result( # type: ignore[call-arg] + [non_dict_result], + MockValidationInfo(field_name="result"), # type: ignore[arg-type] + ) # - Assert - # Verify that the non-dict item is included in the result diff --git a/tests/models/test_incoming_message_validation.py b/tests/models/test_incoming_message_validation.py index 4acb5c66..aa6ad0d5 100644 --- a/tests/models/test_incoming_message_validation.py +++ b/tests/models/test_incoming_message_validation.py @@ -1,4 +1,3 @@ - from pybotx.models.message.incoming_message import BotAPIIncomingMessage from pybotx.models.message.mentions import BotAPIMention @@ -25,7 +24,8 @@ def test_validate_items_dict() -> None: # - Act - # Call validate_items with a list containing a dict item result = BotAPIIncomingMessage.validate_items( - [entity_dict], MockValidationInfo(field_name="entities") # type: ignore[call-arg, arg-type] + [entity_dict], + MockValidationInfo(field_name="entities"), # type: ignore[call-arg, arg-type] ) # - Assert - @@ -43,7 +43,8 @@ def test_validate_items_non_dict() -> None: # Call validate_items with a list containing a non-dict item # The non-dict item will not be processed and not added to the result list result = BotAPIIncomingMessage.validate_items( - [non_dict_entity], MockValidationInfo(field_name="entities") # type: ignore[call-arg, arg-type] + [non_dict_entity], + MockValidationInfo(field_name="entities"), # type: ignore[call-arg, arg-type] ) # - Assert - From 87892f8ca9702b40094d479697496e0fd9a15f62 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 00:56:32 +0300 Subject: [PATCH 05/18] feat: fix ci and lint --- scripts/docs-lint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docs-lint b/scripts/docs-lint index 0bec7894..ed359af6 100755 --- a/scripts/docs-lint +++ b/scripts/docs-lint @@ -15,6 +15,6 @@ while IFS= read -d '' -r snippet || [[ -n "${snippet}" ]]; do done < <(grep -Pzo '(?s)(?<=```python\n).*?(?=```)' README.md); mypy "${snippets_dir_path}" -ruff lint "${snippets_dir_path}" +ruff check "${snippets_dir_path}" rm -rf "${snippets_dir_path}" From 641b1e886ae0fbc155925ce920ff3b28c0e31d80 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 01:07:58 +0300 Subject: [PATCH 06/18] feat: ignore for readme F405 --- scripts/docs-lint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docs-lint b/scripts/docs-lint index ed359af6..49f20b9e 100755 --- a/scripts/docs-lint +++ b/scripts/docs-lint @@ -15,6 +15,6 @@ while IFS= read -d '' -r snippet || [[ -n "${snippet}" ]]; do done < <(grep -Pzo '(?s)(?<=```python\n).*?(?=```)' README.md); mypy "${snippets_dir_path}" -ruff check "${snippets_dir_path}" +ruff check "${snippets_dir_path}" --ignore F405 rm -rf "${snippets_dir_path}" From 439e82fa323dcaad7091ade28a02e50b368cd124 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 01:10:10 +0300 Subject: [PATCH 07/18] feat: ignore for readme F403 --- scripts/docs-lint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/docs-lint b/scripts/docs-lint index 49f20b9e..8636692d 100755 --- a/scripts/docs-lint +++ b/scripts/docs-lint @@ -15,6 +15,6 @@ while IFS= read -d '' -r snippet || [[ -n "${snippet}" ]]; do done < <(grep -Pzo '(?s)(?<=```python\n).*?(?=```)' README.md); mypy "${snippets_dir_path}" -ruff check "${snippets_dir_path}" --ignore F405 +ruff check "${snippets_dir_path}" --ignore F403,F405 rm -rf "${snippets_dir_path}" From 0588d28826600cf66a9584e0e778fb7ea54bf004 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 01:24:29 +0300 Subject: [PATCH 08/18] feat: bump min version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fe2e1a1e..d68b308a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ httpx = "^0.28.0" # https://github.com/encode/httpcore/pull/880 httpcore = "1.0.9" loguru = ">=0.6.0,<0.7.0" -pydantic = ">=2.0,<3.0" +pydantic = ">=2.8.2,<3.0" aiocsv = ">=1.2.3,<=1.4.0" pyjwt = ">=2.0.0,<3.0.0" mypy-extensions = ">=0.2.0,<=1.1.0" From e178bd57c108a9cb3ab674a7a48e73d687d888f2 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 01:27:49 +0300 Subject: [PATCH 09/18] feat: poetry lock --- poetry.lock | 217 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 90 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3e3a2354..0e25552a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aiocsv" @@ -691,111 +691,133 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "2.7.0" +version = "2.11.7" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, - {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.18.1" -typing-extensions = ">=4.6.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" -version = "2.18.1" +version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, - {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, - {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, - {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, - {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, - {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, - {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, - {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, - {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, - {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, - {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, - {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, - {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, - {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, - {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, - {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, - {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, - {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, - {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, - {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, - {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, - {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, - {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, - {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, - {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, - {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, - {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, - {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, - {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, - {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, - {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, - {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, - {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] [package.dependencies] @@ -1166,6 +1188,21 @@ files = [ {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" version = "2025.2" @@ -1235,4 +1272,4 @@ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<3.14" -content-hash = "4b7da3ca1bf8ecf98eb82b5454b28913f35dfbdaaa18d29291c67f3009fa71a3" +content-hash = "a8cf8ae3d3681d747a0416ede87c2c3b4108d6d7989bbc296a19f457c1f220ef" From 8d1269dacde9ce7570ebec20442c4c601d85b7d6 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Sat, 12 Jul 2025 01:33:10 +0300 Subject: [PATCH 10/18] feat: mypy fix --- pybotx/bot/bot.py | 2 +- pybotx/models/message/incoming_message.py | 2 +- tests/client/chats_api/test_chat_info_validation.py | 4 ++-- tests/client/chats_api/test_list_chats_validation.py | 4 ++-- tests/models/test_incoming_message_validation.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index e77efd20..5bba3576 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -423,7 +423,7 @@ async def set_raw_botx_method_result( if verify_request: self._verify_request(request_headers, trusted_issuers=trusted_issuers) - callback = TypeAdapter(BotXMethodCallback).validate_python( + callback: BotXMethodCallback = TypeAdapter(BotXMethodCallback).validate_python( raw_botx_method_result, ) diff --git a/pybotx/models/message/incoming_message.py b/pybotx/models/message/incoming_message.py index 0aca386f..b724d9d4 100644 --- a/pybotx/models/message/incoming_message.py +++ b/pybotx/models/message/incoming_message.py @@ -204,7 +204,7 @@ def validate_items( item_model = ( BotAPIAttachment if info.field_name == "attachments" else BotAPIEntity ) - parsed = [] + parsed: List[Any] = [] for item in value: if isinstance(item, dict): try: diff --git a/tests/client/chats_api/test_chat_info_validation.py b/tests/client/chats_api/test_chat_info_validation.py index 97b5f048..86d38ed3 100644 --- a/tests/client/chats_api/test_chat_info_validation.py +++ b/tests/client/chats_api/test_chat_info_validation.py @@ -25,9 +25,9 @@ def test_validate_members_non_dict() -> None: # - Act - # Call validate_members with a list containing a non-dict item - result = BotXAPIChatInfoResult.validate_members( # type: ignore[call-arg] + result = BotXAPIChatInfoResult.validate_members( [non_dict_member], - MockValidationInfo(field_name="members"), # type: ignore[arg-type] + MockValidationInfo(field_name="members"), ) # - Assert - diff --git a/tests/client/chats_api/test_list_chats_validation.py b/tests/client/chats_api/test_list_chats_validation.py index 6e3214bc..b4a1935a 100644 --- a/tests/client/chats_api/test_list_chats_validation.py +++ b/tests/client/chats_api/test_list_chats_validation.py @@ -31,9 +31,9 @@ def test_validate_result_non_dict() -> None: # - Act - # Call validate_result with a list containing a non-dict item - result = BotXAPIListChatResponsePayload.validate_result( # type: ignore[call-arg] + result = BotXAPIListChatResponsePayload.validate_result( [non_dict_result], - MockValidationInfo(field_name="result"), # type: ignore[arg-type] + MockValidationInfo(field_name="result"), ) # - Assert - diff --git a/tests/models/test_incoming_message_validation.py b/tests/models/test_incoming_message_validation.py index aa6ad0d5..df486b4b 100644 --- a/tests/models/test_incoming_message_validation.py +++ b/tests/models/test_incoming_message_validation.py @@ -25,7 +25,7 @@ def test_validate_items_dict() -> None: # Call validate_items with a list containing a dict item result = BotAPIIncomingMessage.validate_items( [entity_dict], - MockValidationInfo(field_name="entities"), # type: ignore[call-arg, arg-type] + MockValidationInfo(field_name="entities"), ) # - Assert - @@ -44,7 +44,7 @@ def test_validate_items_non_dict() -> None: # The non-dict item will not be processed and not added to the result list result = BotAPIIncomingMessage.validate_items( [non_dict_entity], - MockValidationInfo(field_name="entities"), # type: ignore[call-arg, arg-type] + MockValidationInfo(field_name="entities"), ) # - Assert - From e251120ed660fe1c5489b4ec1e9bfcd207f41138 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Mon, 14 Jul 2025 15:24:17 +0300 Subject: [PATCH 11/18] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=B5:=20"?= =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=87=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=20=D0=B1=D0=BE=D1=82=D0=B0=20=D0=B8=20=D1=8E=D0=B7=D0=B5=D1=80?= =?UTF-8?q?=D0=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pybotx/bot/bot.py | 27 +++ pybotx/bot/handler_collector.py | 2 +- pybotx/client/chats_api/chat_info.py | 47 ++++- pybotx/client/chats_api/list_chats.py | 15 +- pybotx/client/chats_api/personal_chat.py | 152 ++++++++++++++++ pybotx/models/api_base.py | 4 +- pybotx/models/base_command.py | 2 +- pybotx/models/message/incoming_message.py | 25 +-- tests/client/chats_api/factories.py | 72 ++++++++ tests/client/chats_api/test_personal_chat.py | 162 ++++++++++++++++++ .../test_personal_chat_additional.py | 99 +++++++++++ 11 files changed, 580 insertions(+), 27 deletions(-) create mode 100644 pybotx/client/chats_api/personal_chat.py create mode 100644 tests/client/chats_api/factories.py create mode 100644 tests/client/chats_api/test_personal_chat.py create mode 100644 tests/client/chats_api/test_personal_chat_additional.py diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 5bba3576..663180b6 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -53,6 +53,10 @@ BotXAPIChatInfoRequestPayload, ChatInfoMethod, ) +from pybotx.client.chats_api.personal_chat import ( + BotXAPIPersonalChatRequestPayload, + PersonalChatMethod, +) from pybotx.client.chats_api.create_chat import ( BotXAPICreateChatRequestPayload, CreateChatMethod, @@ -1013,6 +1017,29 @@ async def chat_info( return botx_api_chat_info.to_domain() + async def personal_chat( + self, + *, + bot_id: UUID, + user_huid: UUID, + ) -> ChatInfo: + """Get personal chat between bot and user. + + :param bot_id: Bot which should perform the request. + :param user_huid: User identifier. + + :return: Chat information. + """ + + method = PersonalChatMethod( + bot_id, self._httpx_client, self._bot_accounts_storage + ) + + payload = BotXAPIPersonalChatRequestPayload.from_domain(user_huid=user_huid) + botx_api_personal_chat = await method.execute(payload) + + return botx_api_personal_chat.to_domain() + async def add_users_to_chat( self, *, diff --git a/pybotx/bot/handler_collector.py b/pybotx/bot/handler_collector.py index 3d07d146..cdffa6fd 100644 --- a/pybotx/bot/handler_collector.py +++ b/pybotx/bot/handler_collector.py @@ -119,7 +119,7 @@ async def handle_bot_command(self, bot_command: BotCommand, bot: "Bot") -> None: elif isinstance( bot_command, - SystemEvent.__args__, # type: ignore [attr-defined] # noqa: WPS609 + SystemEvent.__args__, # type: ignore [attr-defined] ): event_handler = self._get_system_event_handler_or_none(bot_command) if event_handler: diff --git a/pybotx/client/chats_api/chat_info.py b/pybotx/client/chats_api/chat_info.py index 23e52254..7be54ace 100644 --- a/pybotx/client/chats_api/chat_info.py +++ b/pybotx/client/chats_api/chat_info.py @@ -2,12 +2,12 @@ from typing import Any, Dict, List, Literal, Optional, Union from uuid import UUID +from pydantic import ConfigDict, ValidationError, field_validator from pybotx.client.authorized_botx_method import AuthorizedBotXMethod from pybotx.client.botx_method import response_exception_thrower from pybotx.client.exceptions.common import ChatNotFoundError from pybotx.logger import logger from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel -from pydantic import ConfigDict, ValidationError, field_validator from pybotx.models.chats import ChatInfo, ChatInfoMember from pybotx.models.enums import ( APIChatTypes, @@ -20,16 +20,23 @@ class BotXAPIChatInfoRequestPayload(UnverifiedPayloadBaseModel): group_chat_id: UUID + model_config = ConfigDict(extra="forbid") + @classmethod def from_domain(cls, chat_id: UUID) -> "BotXAPIChatInfoRequestPayload": return cls(group_chat_id=chat_id) + def as_query_params(self) -> Dict[str, Any]: + return self.model_dump(mode="json") + class BotXAPIChatInfoMember(VerifiedPayloadBaseModel): admin: bool user_huid: UUID user_kind: APIUserKinds + model_config = ConfigDict(extra="ignore") + class BotXAPIChatInfoResult(VerifiedPayloadBaseModel): chat_type: APIChatTypes @@ -37,33 +44,55 @@ class BotXAPIChatInfoResult(VerifiedPayloadBaseModel): description: Optional[str] = None group_chat_id: UUID inserted_at: dt - members: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] # noqa: WPS234 + members: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] = [] name: str shared_history: bool - model_config = ConfigDict() + model_config = ConfigDict(extra="ignore") - @field_validator("members", mode="before") - @classmethod + @staticmethod def validate_members( - cls, value: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]], info: Any + items: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]], + info: Any, ) -> List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]: + """ + Публичный helper для парсинга списка участников: + - dict → BotXAPIChatInfoMember + - уже готовый BotXAPIChatInfoMember остаётся как есть + - всё остальное логируется и пропускается + """ parsed: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]] = [] - for item in value: + for item in items: if isinstance(item, dict): try: parsed.append(BotXAPIChatInfoMember.model_validate(item)) except ValidationError: + # Сохраняем оригинал, чтобы downstream-логика могла + # увидеть и обработать «неожиданную» структуру parsed.append(item) - else: + logger.warning("Unsupported member structure encountered: %s", item) + elif isinstance(item, BotXAPIChatInfoMember): parsed.append(item) + else: + logger.warning("Unknown member type: %s", item) # pragma: no cover return parsed + @field_validator("members", mode="before") + @classmethod + def _validate_members_field( + cls, + value: List[Union[BotXAPIChatInfoMember, Dict[str, Any]]], + info: Any, + ) -> List[Union[BotXAPIChatInfoMember, Dict[str, Any]]]: + return cls.validate_members(value, info) + class BotXAPIChatInfoResponsePayload(VerifiedPayloadBaseModel): status: Literal["ok"] result: BotXAPIChatInfoResult + model_config = ConfigDict(extra="ignore") + def to_domain(self) -> ChatInfo: if any(isinstance(member, dict) for member in self.result.members): logger.warning("One or more unsupported user types skipped") @@ -105,7 +134,7 @@ async def execute( response = await self._botx_method_call( "GET", self._build_url(path), - params=payload.jsonable_dict(), + params=payload.as_query_params(), ) return self._verify_and_extract_api_model( diff --git a/pybotx/client/chats_api/list_chats.py b/pybotx/client/chats_api/list_chats.py index fd833d4c..c692ff2d 100644 --- a/pybotx/client/chats_api/list_chats.py +++ b/pybotx/client/chats_api/list_chats.py @@ -23,12 +23,11 @@ class BotXAPIListChatResult(VerifiedPayloadBaseModel): class BotXAPIListChatResponsePayload(VerifiedPayloadBaseModel): status: Literal["ok"] - result: List[Union[BotXAPIListChatResult, Dict[str, Any]]] # noqa: WPS234 + result: List[Union[BotXAPIListChatResult, Dict[str, Any]]] - @field_validator("result", mode="before") - @classmethod + @staticmethod def validate_result( - cls, value: List[Union[BotXAPIListChatResult, Dict[str, Any]]], info: Any + value: List[Union[BotXAPIListChatResult, Dict[str, Any]]], info: Any ) -> List[Union[BotXAPIListChatResult, Dict[str, Any]]]: parsed: List[Union[BotXAPIListChatResult, Dict[str, Any]]] = [] for item in value: @@ -41,6 +40,14 @@ def validate_result( parsed.append(item) return parsed + @field_validator("result", mode="before") + @classmethod + def _validate_result_field( + cls, value: List[Union[BotXAPIListChatResult, Dict[str, Any]]], info: Any + ) -> List[Union[BotXAPIListChatResult, Dict[str, Any]]]: + # Pydantic-валидатор: просто делегируем статическому методу + return cls.validate_result(value, info) + def to_domain(self) -> List[ChatListItem]: chats_list = [ ChatListItem( diff --git a/pybotx/client/chats_api/personal_chat.py b/pybotx/client/chats_api/personal_chat.py new file mode 100644 index 00000000..40d33fce --- /dev/null +++ b/pybotx/client/chats_api/personal_chat.py @@ -0,0 +1,152 @@ +from datetime import datetime as dt +from typing import Any, Dict, List, Literal, Optional, Union +from uuid import UUID + +from pydantic import ConfigDict, ValidationError, field_validator, Field +from pybotx.client.authorized_botx_method import AuthorizedBotXMethod +from pybotx.client.botx_method import response_exception_thrower +from pybotx.client.exceptions.common import ChatNotFoundError +from pybotx.logger import logger +from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel +from pybotx.models.chats import ChatInfo, ChatInfoMember +from pybotx.models.enums import ( + APIChatTypes, + APIUserKinds, + convert_chat_type_to_domain, + convert_user_kind_to_domain, +) + + +class BotXAPIPersonalChatRequestPayload(UnverifiedPayloadBaseModel): + """Payload запроса на получение персонального чата по HUID пользователя.""" + + user_huid: UUID + + model_config = ConfigDict(extra="forbid") + + @classmethod + def from_domain(cls, user_huid: UUID) -> "BotXAPIPersonalChatRequestPayload": + return cls(user_huid=user_huid) + + def as_query_params(self) -> Dict[str, Any]: + return self.model_dump(mode="json") + + +class BotXAPIPersonalChatMember(VerifiedPayloadBaseModel): + """Информация об участнике чата.""" + + admin: bool + user_huid: UUID + user_kind: APIUserKinds + + model_config = ConfigDict(extra="forbid") + + +class BotXAPIPersonalChatResult(VerifiedPayloadBaseModel): + """Результат API-ответа по персональному чату.""" + + chat_type: APIChatTypes + creator: Optional[UUID] + description: Optional[str] = None + group_chat_id: UUID + inserted_at: dt + members: List[Union[BotXAPIPersonalChatMember, Dict[str, Any]]] = Field( + default_factory=list + ) + name: str + shared_history: bool + + model_config = ConfigDict(extra="forbid") + + @field_validator("members", mode="before") + @classmethod + def validate_members( + cls, + value: List[Union[BotXAPIPersonalChatMember, Dict[str, Any]]], + info: Any, + ) -> List[Union[BotXAPIPersonalChatMember, Dict[str, Any]]]: + return cls._parse_members(value) + + @staticmethod + def _parse_members( + members_data: List[Union[BotXAPIPersonalChatMember, Dict[str, Any]]], + ) -> List[Union[BotXAPIPersonalChatMember, Dict[str, Any]]]: + # Явная аннотация решает проблему инвариантности List в mypy + parsed: List[Union[BotXAPIPersonalChatMember, Dict[str, Any]]] = [] + for item in members_data: + if isinstance(item, dict): + try: + parsed.append(BotXAPIPersonalChatMember.model_validate(item)) + except ValidationError: + logger.warning("Unsupported member structure encountered: %s", item) + parsed.append(item) + elif isinstance(item, BotXAPIPersonalChatMember): + parsed.append(item) + else: + logger.warning("Unknown member type: %s", item) + return parsed + + +class BotXAPIPersonalChatResponsePayload(VerifiedPayloadBaseModel): + """Обработанный payload успешного ответа API по персональному чату.""" + + status: Literal["ok"] + result: BotXAPIPersonalChatResult + + model_config = ConfigDict(extra="forbid") + + def to_domain(self) -> ChatInfo: + members: List[ChatInfoMember] = [] + for member in self.result.members: + if isinstance(member, BotXAPIPersonalChatMember): + try: + members.append( + ChatInfoMember( + is_admin=member.admin, + huid=member.user_huid, + kind=convert_user_kind_to_domain(member.user_kind), + ) + ) + except Exception as exc: + logger.warning("Failed to convert member kind: %s", exc) + else: + logger.warning( + "Unsupported user type skipped in members list: %s", member + ) + + return ChatInfo( + chat_type=convert_chat_type_to_domain(self.result.chat_type), + creator_id=self.result.creator, + description=self.result.description, + chat_id=self.result.group_chat_id, + created_at=self.result.inserted_at, + members=members, + name=self.result.name, + shared_history=self.result.shared_history, + ) + + +class PersonalChatMethod(AuthorizedBotXMethod): + """Метод получения информации о персональном чате по HUID пользователя.""" + + status_handlers = { + **AuthorizedBotXMethod.status_handlers, + 404: response_exception_thrower(ChatNotFoundError), + } + + async def execute( + self, + payload: BotXAPIPersonalChatRequestPayload, + ) -> BotXAPIPersonalChatResponsePayload: + path = "/api/v1/botx/chats/personal" + + response = await self._botx_method_call( + "GET", + self._build_url(path), + params=payload.as_query_params(), + ) + + return self._verify_and_extract_api_model( + BotXAPIPersonalChatResponsePayload, + response, + ) diff --git a/pybotx/models/api_base.py b/pybotx/models/api_base.py index e8755f92..a4e0fbe9 100644 --- a/pybotx/models/api_base.py +++ b/pybotx/models/api_base.py @@ -45,7 +45,7 @@ def _remove_undefined( class PayloadBaseModel(BaseModel): - def json(self) -> str: # type: ignore [override] + def json(self) -> str: # type: ignore[override] clean_dict = _remove_undefined(self.model_dump()) return json.dumps(clean_dict, default=to_jsonable_python, ensure_ascii=False) @@ -67,6 +67,6 @@ def __init__( **kwargs: Any, ) -> None: model = self.__class__.model_construct(_fields_set=_fields_set, **kwargs) - self.__dict__.update(model.__dict__) # noqa: WPS609 (Replace self attrs) + self.__dict__.update(model.__dict__) model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/pybotx/models/base_command.py b/pybotx/models/base_command.py index c27440ad..d710f50a 100644 --- a/pybotx/models/base_command.py +++ b/pybotx/models/base_command.py @@ -81,7 +81,7 @@ class BotAPIBaseSystemEventPayload(VerifiedPayloadBaseModel): @field_validator("body", mode="before", check_fields=False) @classmethod def find_unknown_system_event(cls, body: str) -> str: - if body not in BotAPISystemEventTypes.__members__.values(): # noqa: WPS609 + if body not in BotAPISystemEventTypes.__members__.values(): raise UnknownSystemEventError(body) return body diff --git a/pybotx/models/message/incoming_message.py b/pybotx/models/message/incoming_message.py index b724d9d4..2f16604b 100644 --- a/pybotx/models/message/incoming_message.py +++ b/pybotx/models/message/incoming_message.py @@ -193,14 +193,11 @@ class BotAPIIncomingMessage(BotAPIBaseCommand): sender: BotAPIIncomingMessageContext = Field(..., alias="from") source_sync_id: Optional[UUID] - attachments: List[Union[BotAPIAttachment, Dict[str, Any]]] # noqa: WPS234 - entities: List[Union[BotAPIEntity, Dict[str, Any]]] # noqa: WPS234 + attachments: List[Union[BotAPIAttachment, Dict[str, Any]]] + entities: List[Union[BotAPIEntity, Dict[str, Any]]] - @field_validator("attachments", "entities", mode="before") - @classmethod - def validate_items( - cls, value: List[Union[Dict[str, Any], Any]], info: Any - ) -> List[Any]: + @staticmethod + def validate_items(value: List[Union[Dict[str, Any], Any]], info: Any) -> List[Any]: item_model = ( BotAPIAttachment if info.field_name == "attachments" else BotAPIEntity ) @@ -213,7 +210,15 @@ def validate_items( parsed.append(item) return parsed - def to_domain(self, raw_command: Dict[str, Any]) -> IncomingMessage: # noqa: WPS231 + @field_validator("attachments", "entities", mode="before") + @classmethod + def _validate_items_field( + cls, value: List[Union[Dict[str, Any], Any]], info: Any + ) -> List[Any]: + # Pydantic-валидатор: просто делегируем статическому методу + return cls.validate_items(value, info) + + def to_domain(self, raw_command: Dict[str, Any]) -> IncomingMessage: if self.sender.device_meta: pushes = self.sender.device_meta.pushes timezone = self.sender.device_meta.timezone @@ -269,7 +274,7 @@ def to_domain(self, raw_command: Dict[str, Any]) -> IncomingMessage: # noqa: WP self.attachments[0], self.payload.body, ) - if isinstance(attachment_domain, FileAttachmentBase): # noqa: WPS223 + if isinstance(attachment_domain, FileAttachmentBase): file = attachment_domain elif isinstance(attachment_domain, Location): location = attachment_domain @@ -292,7 +297,7 @@ def to_domain(self, raw_command: Dict[str, Any]) -> IncomingMessage: # noqa: WP entity_domain = convert_bot_api_entity_to_domain(entity) if isinstance( entity_domain, - Mention.__args__, # type: ignore [attr-defined] # noqa: WPS609 + Mention.__args__, # type: ignore [attr-defined] ): mentions.append(entity_domain) elif isinstance(entity_domain, Forward): diff --git a/tests/client/chats_api/factories.py b/tests/client/chats_api/factories.py new file mode 100644 index 00000000..df64dae6 --- /dev/null +++ b/tests/client/chats_api/factories.py @@ -0,0 +1,72 @@ +from datetime import datetime +from uuid import UUID + +from factory.base import Factory, DictFactory +from factory.declarations import LazyFunction, SubFactory + +from pybotx import ChatInfo, ChatInfoMember, ChatTypes, UserKinds + + +class ChatInfoMemberFactory(Factory[ChatInfoMember]): + class Meta: + model = ChatInfoMember + + is_admin = False + huid = LazyFunction(lambda: UUID("705df263-6bfd-536a-9d51-13524afaab5c")) # type: ignore[no-untyped-call] + kind = UserKinds.BOT + + +class ChatInfoFactory(Factory[ChatInfo]): + class Meta: + model = ChatInfo + + chat_type = ChatTypes.GROUP_CHAT + creator_id = LazyFunction(lambda: UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364")) # type: ignore[no-untyped-call] + description = "Desc" + chat_id = LazyFunction(lambda: UUID("740cf331-d833-5250-b5a5-5b5cbc697ff5")) # type: ignore[no-untyped-call] + created_at = LazyFunction( + lambda: datetime.fromisoformat("2019-08-29T11:22:48.358586Z") + ) # type: ignore[no-untyped-call] + members = LazyFunction( + lambda: [ + ChatInfoMemberFactory( # type: ignore[no-untyped-call] + is_admin=True, + huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + kind=UserKinds.RTS_USER, + ), + ChatInfoMemberFactory(), # type: ignore[no-untyped-call] + ] + ) + name = "Chat Name" + shared_history = False + + +class APIChatMemberFactory(DictFactory): + admin = False + user_huid = "705df263-6bfd-536a-9d51-13524afaab5c" + user_kind = "botx" + + +class APIPersonalChatResultFactory(DictFactory): + chat_type = "group_chat" + creator = "6fafda2c-6505-57a5-a088-25ea5d1d0364" + description = "Desc" + group_chat_id = "740cf331-d833-5250-b5a5-5b5cbc697ff5" + inserted_at = "2019-08-29T11:22:48.358586Z" + members = LazyFunction( + lambda: [ + APIChatMemberFactory( # type: ignore[no-untyped-call] + admin=True, + user_huid="6fafda2c-6505-57a5-a088-25ea5d1d0364", + user_kind="user", + ), + APIChatMemberFactory(), # type: ignore[no-untyped-call] + ] + ) + name = "Chat Name" + shared_history = False + + +class APIPersonalChatResponseFactory(DictFactory): + status = "ok" + result = SubFactory(APIPersonalChatResultFactory) # type: ignore[no-untyped-call] diff --git a/tests/client/chats_api/test_personal_chat.py b/tests/client/chats_api/test_personal_chat.py new file mode 100644 index 00000000..99449007 --- /dev/null +++ b/tests/client/chats_api/test_personal_chat.py @@ -0,0 +1,162 @@ +# tests/client/chats_api/test_personal_chat.py + +from datetime import datetime as dt +from http import HTTPStatus +from typing import Callable, Any +from uuid import UUID + +import httpx +import pytest +from deepdiff import DeepDiff +from respx.router import MockRouter + +from pybotx import ( + Bot, + BotAccountWithSecret, + ChatNotFoundError, + HandlerCollector, + lifespan_wrapper, +) +from tests.client.chats_api.factories import ( + APIPersonalChatResponseFactory, + ChatInfoFactory, +) + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + + +async def test__personal_chat__chat_not_found_error_raised( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + endpoint = respx_mock.get( + f"https://{host}/api/v1/botx/chats/personal", + headers={"Authorization": "Bearer token"}, + params={"user_huid": "dcfa5a7c-7cc4-4c89-b6c0-80325604f9f4"}, + ).mock( + return_value=httpx.Response( + HTTPStatus.NOT_FOUND, + json={ + "status": "error", + "reason": "chat_not_found", + "errors": [], + "error_data": { + "user_huid": "dcfa5a7c-7cc4-4c89-b6c0-80325604f9f4", + "error_description": "Chat with user dcfa5a7c-7cc4-4c89-b6c0-80325604f9f4 not found", + }, + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(ChatNotFoundError) as exc: + await bot.personal_chat( + bot_id=bot_id, + user_huid=UUID("dcfa5a7c-7cc4-4c89-b6c0-80325604f9f4"), + ) + + # - Assert - + assert "chat_not_found" in str(exc.value) + assert endpoint.called + + +async def test__personal_chat__succeed( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + datetime_formatter: Callable[[str], dt], + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + api_response: Any = APIPersonalChatResponseFactory() # type: ignore[no-untyped-call] + + endpoint = respx_mock.get( + f"https://{host}/api/v1/botx/chats/personal", + headers={"Authorization": "Bearer token"}, + params={"user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364"}, + ).mock( + return_value=httpx.Response( + HTTPStatus.OK, + json=api_response, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + chat_info = await bot.personal_chat( + bot_id=bot_id, + user_huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + ) + + # - Assert - + expected_chat_info = ChatInfoFactory( + created_at=datetime_formatter("2019-08-29T11:22:48.358586Z"), + ) # type: ignore[no-untyped-call] + + diff = DeepDiff(chat_info, expected_chat_info) + assert diff == {}, diff + + assert endpoint.called + + +async def test__personal_chat__skipped_members( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + datetime_formatter: Callable[[str], dt], + bot_account: BotAccountWithSecret, + loguru_caplog: pytest.LogCaptureFixture, +) -> None: + # - Arrange - + api_response: Any = APIPersonalChatResponseFactory() # type: ignore[no-untyped-call] + # Add an unsupported user type to the members list + api_response["result"]["members"].append( + { + "admin": "not-a-bool", + "user_huid": "5f5c9b04-f7d2-45a6-b36b-0123456789ab", + "user_kind": "unsupported_user_type", + } + ) + + endpoint = respx_mock.get( + f"https://{host}/api/v1/botx/chats/personal", + headers={"Authorization": "Bearer token"}, + params={"user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364"}, + ).mock( + return_value=httpx.Response( + HTTPStatus.OK, + json=api_response, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + chat_info = await bot.personal_chat( + bot_id=bot_id, + user_huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + ) + + # - Assert - + expected_chat_info = ChatInfoFactory( + created_at=datetime_formatter("2019-08-29T11:22:48.358586Z"), + ) # type: ignore[no-untyped-call] + + diff = DeepDiff(chat_info, expected_chat_info) + assert diff == {}, diff + + assert "Unsupported user type skipped in members list" in loguru_caplog.text + assert endpoint.called diff --git a/tests/client/chats_api/test_personal_chat_additional.py b/tests/client/chats_api/test_personal_chat_additional.py new file mode 100644 index 00000000..54254027 --- /dev/null +++ b/tests/client/chats_api/test_personal_chat_additional.py @@ -0,0 +1,99 @@ +import uuid +from datetime import datetime +from typing import Any, Dict +from uuid import UUID + + +import pybotx.client.chats_api.personal_chat as personal_chat_module +from pybotx.client.chats_api.personal_chat import ( + BotXAPIPersonalChatRequestPayload, + BotXAPIPersonalChatResult, + BotXAPIPersonalChatMember, + BotXAPIPersonalChatResponsePayload, +) +from pybotx.models.enums import APIUserKinds, APIChatTypes + + +def test_request_payload_as_query_params_returns_string_uuid() -> None: + """Проверяем, что as_query_params сериализует UUID в строку.""" + huid = uuid.uuid4() + payload = BotXAPIPersonalChatRequestPayload.from_domain(huid) + params = payload.as_query_params() + assert isinstance(params["user_huid"], str) + assert params["user_huid"] == str(huid) + + +def test_parse_members_various_types() -> None: + """Проверяем все ветки _parse_members: dict → модель, готовый экземпляр и неизвестный тип.""" + uid = uuid.uuid4() + + dict_valid: Dict[str, Any] = { + "admin": True, + "user_huid": str(uid), + "user_kind": APIUserKinds.USER.value, + } + member_instance = BotXAPIPersonalChatMember( + admin=False, user_huid=uid, user_kind=APIUserKinds.USER + ) + unknown_item: Any = 12345 + + parsed = BotXAPIPersonalChatResult._parse_members( + [dict_valid, member_instance, unknown_item] + ) + + assert len(parsed) == 2 + assert isinstance(parsed[0], BotXAPIPersonalChatMember) + assert parsed[1] is member_instance + + +def test_to_domain_handles_conversion_error(monkeypatch: Any) -> None: + """Если convert_user_kind_to_domain падает — to_domain игнорирует участника.""" + uid = UUID("00000000-0000-0000-0000-000000000001") + member = BotXAPIPersonalChatMember( + admin=True, user_huid=uid, user_kind=APIUserKinds.USER + ) + result = BotXAPIPersonalChatResult( + chat_type=APIChatTypes.CHAT, + creator=None, + description=None, + group_chat_id=uid, + inserted_at=datetime.utcnow(), + members=[member], + name="test", + shared_history=False, + ) + payload = BotXAPIPersonalChatResponsePayload(status="ok", result=result) + + def fake_convert(kind: Any) -> None: + raise RuntimeError("boom") + + monkeypatch.setattr( + personal_chat_module, + "convert_user_kind_to_domain", + fake_convert, + ) + + chat_info = payload.to_domain() + assert chat_info.members == [] + assert chat_info.chat_id == uid + + +def test_to_domain_skips_unsupported_member_type() -> None: + """Если в result.members передан не-BotXAPIPersonalChatMember — пропускаем.""" + uid = UUID("00000000-0000-0000-0000-000000000002") + unsupported: Dict[str, Any] = {"foo": "bar"} + result = BotXAPIPersonalChatResult( + chat_type=APIChatTypes.CHAT, + creator=None, + description=None, + group_chat_id=uid, + inserted_at=datetime.utcnow(), + members=[unsupported], + name="test", + shared_history=False, + ) + payload = BotXAPIPersonalChatResponsePayload(status="ok", result=result) + + chat_info = payload.to_domain() + assert chat_info.members == [] + assert chat_info.chat_id == uid From c91dbc4b015e625de2115ff9949268b731cfdcd5 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Mon, 14 Jul 2025 15:26:00 +0300 Subject: [PATCH 12/18] feat: some fix --- pybotx/client/botx_method.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybotx/client/botx_method.py b/pybotx/client/botx_method.py index 99b18514..34a8f2c1 100644 --- a/pybotx/client/botx_method.py +++ b/pybotx/client/botx_method.py @@ -87,7 +87,7 @@ async def execute(self, *args: Any, **kwargs: Any) -> Any: # type: ignore raise NotImplementedError("You should define `execute` method") def _build_url(self, path: str) -> str: - cts_url = str(self._bot_accounts_storage.get_cts_url(self._bot_id)) + cts_url = self._bot_accounts_storage.get_cts_url(self._bot_id) return "/".join(part.strip("/") for part in (cts_url, path)) def _verify_and_extract_api_model( From 2f273b00eb8e75e74548479ac737f6ffccdaecca Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Wed, 16 Jul 2025 13:44:00 +0300 Subject: [PATCH 13/18] feat: add avatar for create_chat and some fix --- pybotx/bot/bot.py | 7 +- pybotx/client/chats_api/chat_info.py | 2 +- pybotx/client/chats_api/create_chat.py | 82 +++++++++++----- pybotx/models/enums.py | 5 + tests/client/chats_api/test_create_chat.py | 107 +++++++++++++++++++++ tests/models/test_enums.py | 24 +++++ 6 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 tests/models/test_enums.py diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 663180b6..e8ea596c 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -1178,6 +1178,7 @@ async def create_chat( huids: List[UUID], description: Optional[str] = None, shared_history: Missing[bool] = Undefined, + avatar: Optional[str] = None, ) -> UUID: """Create chat. @@ -1188,6 +1189,7 @@ async def create_chat( :param description: Chat description. :param shared_history: (BotX default: False) Open old chat history for new added users. + :param avatar: Chat avatar in data URL format (RFC 2397). :return: Created chat uuid. """ @@ -1198,12 +1200,13 @@ async def create_chat( self._bot_accounts_storage, ) - payload = BotXAPICreateChatRequestPayload.from_domain( + payload = BotXAPICreateChatRequestPayload( name=name, chat_type=chat_type, - huids=huids, + members=huids, shared_history=shared_history, description=description, + avatar=avatar, ) botx_api_chat_id = await method.execute(payload) diff --git a/pybotx/client/chats_api/chat_info.py b/pybotx/client/chats_api/chat_info.py index 7be54ace..b4d9e47f 100644 --- a/pybotx/client/chats_api/chat_info.py +++ b/pybotx/client/chats_api/chat_info.py @@ -74,7 +74,7 @@ def validate_members( elif isinstance(item, BotXAPIChatInfoMember): parsed.append(item) else: - logger.warning("Unknown member type: %s", item) # pragma: no cover + logger.warning("Unknown member type: %s", item) # pragma: no cover return parsed @field_validator("members", mode="before") diff --git a/pybotx/client/chats_api/create_chat.py b/pybotx/client/chats_api/create_chat.py index 9da8f761..7bc1cc37 100644 --- a/pybotx/client/chats_api/create_chat.py +++ b/pybotx/client/chats_api/create_chat.py @@ -1,47 +1,72 @@ -from typing import List, Literal, Optional +from typing import Any, Dict, List, Literal, Optional, Set, Union from uuid import UUID +from pydantic import ( + Field, + ConfigDict, + field_serializer, + field_validator, + model_validator, +) + from pybotx.client.authorized_botx_method import AuthorizedBotXMethod from pybotx.client.botx_method import response_exception_thrower from pybotx.client.exceptions.chats import ( ChatCreationError, ChatCreationProhibitedError, ) -from pybotx.missing import Missing +from pybotx.missing import Missing, Undefined from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel +from pybotx.models.attachments import decode_rfc2397 from pybotx.models.enums import APIChatTypes, ChatTypes, convert_chat_type_from_domain class BotXAPICreateChatRequestPayload(UnverifiedPayloadBaseModel): - name: str - description: Optional[str] - chat_type: APIChatTypes + model_config = ConfigDict( + validate_assignment=True, + frozen=True, + str_strip_whitespace=True, + use_enum_values=True, + ) + + name: str = Field(..., min_length=1) + description: Optional[str] = None + chat_type: Union[APIChatTypes, ChatTypes] members: List[UUID] shared_history: Missing[bool] + avatar: Optional[str] = None - @classmethod - def from_domain( - cls, - name: str, - chat_type: ChatTypes, - huids: List[UUID], - shared_history: Missing[bool], - description: Optional[str] = None, - ) -> "BotXAPICreateChatRequestPayload": - return cls( - name=name, - chat_type=convert_chat_type_from_domain(chat_type), - members=huids, - description=description, - shared_history=shared_history, - ) + @model_validator(mode="before") + def _convert_chat_type(cls, values: Dict[str, Any]) -> Dict[str, Any]: + ct = values.get("chat_type") + if isinstance(ct, ChatTypes): + values["chat_type"] = convert_chat_type_from_domain(ct) + return values + + @field_validator("avatar") + def _validate_avatar(cls, v: Optional[str]) -> Optional[str]: + if v is None: + return None + if not v.startswith("data:"): + raise ValueError("Avatar must be a data URL (RFC2397)") + try: + decode_rfc2397(v) + except Exception as e: + raise ValueError(f"Invalid data URL format: {e}") + return v + + @field_serializer("chat_type") + def _serialize_chat_type(self, v: APIChatTypes) -> str: + return v.value.lower() class BotXAPIChatIdResult(VerifiedPayloadBaseModel): + model_config = ConfigDict(frozen=True) chat_id: UUID class BotXAPICreateChatResponsePayload(VerifiedPayloadBaseModel): + model_config = ConfigDict(frozen=True) status: Literal["ok"] result: BotXAPIChatIdResult @@ -60,12 +85,21 @@ async def execute( self, payload: BotXAPICreateChatRequestPayload, ) -> BotXAPICreateChatResponsePayload: - path = "/api/v3/botx/chats/create" + """ + Создаёт чат через BotX API. + """ + url = self._build_url("/api/v3/botx/chats/create") + + exclude: Set[str] = ( + {"shared_history"} if payload.shared_history is Undefined else set() + ) + + body = payload.model_dump(mode="json", exclude=exclude) response = await self._botx_method_call( "POST", - self._build_url(path), - json=payload.jsonable_dict(), + url, + json=body, ) return self._verify_and_extract_api_model( diff --git a/pybotx/models/enums.py b/pybotx/models/enums.py index 05bc35fa..1a4bc0e0 100644 --- a/pybotx/models/enums.py +++ b/pybotx/models/enums.py @@ -54,11 +54,13 @@ class ChatTypes(AutoName): PERSONAL_CHAT: Personal chat with user. GROUP_CHAT: Group chat. CHANNEL: Public channel. + THREAD: Thread in a chat. """ PERSONAL_CHAT = auto() GROUP_CHAT = auto() CHANNEL = auto() + THREAD = auto() class SyncSourceTypes(AutoName): @@ -92,6 +94,7 @@ class APIChatTypes(Enum): CHAT = "chat" GROUP_CHAT = "group_chat" CHANNEL = "channel" + THREAD = "thread" class BotAPICommandTypes(StrEnum): @@ -295,6 +298,7 @@ def convert_chat_type_from_domain(chat_type: ChatTypes) -> APIChatTypes: ChatTypes.PERSONAL_CHAT: APIChatTypes.CHAT, ChatTypes.GROUP_CHAT: APIChatTypes.GROUP_CHAT, ChatTypes.CHANNEL: APIChatTypes.CHANNEL, + ChatTypes.THREAD: APIChatTypes.THREAD, } converted_type = chat_types_mapping.get(chat_type) @@ -323,6 +327,7 @@ def convert_chat_type_to_domain( APIChatTypes.CHAT: ChatTypes.PERSONAL_CHAT, APIChatTypes.GROUP_CHAT: ChatTypes.GROUP_CHAT, APIChatTypes.CHANNEL: ChatTypes.CHANNEL, + APIChatTypes.THREAD: ChatTypes.THREAD, } converted_type: Optional[IncomingChatTypes] diff --git a/tests/client/chats_api/test_create_chat.py b/tests/client/chats_api/test_create_chat.py index 10bc5f88..ff0eec93 100644 --- a/tests/client/chats_api/test_create_chat.py +++ b/tests/client/chats_api/test_create_chat.py @@ -37,6 +37,7 @@ async def test__create_chat__bot_have_no_permissions_raised( "description": None, "chat_type": "group_chat", "members": [], + "avatar": None, }, ).mock( return_value=httpx.Response( @@ -84,6 +85,7 @@ async def test__create_chat__botx_error_raised( "description": None, "chat_type": "group_chat", "members": [], + "avatar": None, }, ).mock( return_value=httpx.Response( @@ -130,6 +132,7 @@ async def test__create_chat__maximum_filled_succeed( "chat_type": "group_chat", "members": ["2fc83441-366a-49ba-81fc-6c39f065bb58"], "shared_history": True, + "avatar": None, }, ).mock( return_value=httpx.Response( @@ -157,3 +160,107 @@ async def test__create_chat__maximum_filled_succeed( # - Assert - assert created_chat_id == UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa") assert endpoint.called + + +def test__create_chat_payload__invalid_avatar_non_data_url_error() -> None: + """Test that BotXAPICreateChatRequestPayload validator raises ValueError for non-data URL avatar.""" + from pybotx.client.chats_api.create_chat import BotXAPICreateChatRequestPayload + + # Test the validator directly since UnverifiedPayloadBaseModel bypasses validation + # The validator is a classmethod that expects (cls, value) + with pytest.raises(ValueError, match="Avatar must be a data URL \\(RFC2397\\)"): + BotXAPICreateChatRequestPayload._validate_avatar("invalid-url") # type: ignore[call-arg] + + +def test__create_chat_payload__invalid_avatar_bad_rfc2397_format_error() -> None: + """Test that BotXAPICreateChatRequestPayload validator raises ValueError for invalid RFC2397 format.""" + from pybotx.client.chats_api.create_chat import BotXAPICreateChatRequestPayload + + # Test the validator directly since UnverifiedPayloadBaseModel bypasses validation + with pytest.raises(ValueError, match="Invalid data URL format"): + BotXAPICreateChatRequestPayload._validate_avatar("data:invalid-format") # type: ignore[call-arg] + + +def test__create_chat_payload__avatar_validator_with_none() -> None: + """Test that BotXAPICreateChatRequestPayload avatar validator handles None correctly.""" + from pybotx.client.chats_api.create_chat import BotXAPICreateChatRequestPayload + + # Test the validator directly with None value + result = BotXAPICreateChatRequestPayload._validate_avatar(None) # type: ignore[call-arg] + assert result is None + + +def test__create_chat_payload__avatar_validator_with_valid_data_url() -> None: + """Test that BotXAPICreateChatRequestPayload avatar validator handles valid data URL correctly.""" + from pybotx.client.chats_api.create_chat import BotXAPICreateChatRequestPayload + + valid_avatar = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" + result = BotXAPICreateChatRequestPayload._validate_avatar(valid_avatar) # type: ignore[call-arg] + assert result == valid_avatar + + +def test__create_chat_payload__convert_chat_type_validator() -> None: + """Test that BotXAPICreateChatRequestPayload converts ChatTypes to APIChatTypes.""" + from pybotx.client.chats_api.create_chat import BotXAPICreateChatRequestPayload + from pybotx.models.enums import ChatTypes, APIChatTypes + + # Test with ChatTypes enum + values = {"chat_type": ChatTypes.GROUP_CHAT} + result = BotXAPICreateChatRequestPayload._convert_chat_type(values) # type: ignore[operator] + assert result["chat_type"] == APIChatTypes.GROUP_CHAT + + # Test with non-ChatTypes value (should remain unchanged) + values = {"chat_type": APIChatTypes.CHAT} # type: ignore[dict-item] + result = BotXAPICreateChatRequestPayload._convert_chat_type(values) # type: ignore[operator] + assert result["chat_type"] == APIChatTypes.CHAT + + # Test with missing chat_type key + values = {"name": "test"} # type: ignore[dict-item] + result = BotXAPICreateChatRequestPayload._convert_chat_type(values) # type: ignore[operator] + assert result == {"name": "test"} + + +async def test__create_chat__with_valid_avatar_succeed( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + valid_avatar = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" + + endpoint = respx_mock.post( + f"https://{host}/api/v3/botx/chats/create", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={ + "name": "Test chat name", + "description": None, + "chat_type": "group_chat", + "members": [], + "avatar": valid_avatar, + }, + ).mock( + return_value=httpx.Response( + HTTPStatus.OK, + json={ + "status": "ok", + "result": {"chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa"}, + }, + ), + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + created_chat_id = await bot.create_chat( + bot_id=bot_id, + name="Test chat name", + chat_type=ChatTypes.GROUP_CHAT, + huids=[], + avatar=valid_avatar, + ) + + # - Assert - + assert created_chat_id == UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa") + assert endpoint.called diff --git a/tests/models/test_enums.py b/tests/models/test_enums.py new file mode 100644 index 00000000..d7d39fb7 --- /dev/null +++ b/tests/models/test_enums.py @@ -0,0 +1,24 @@ +import pytest +from unittest.mock import Mock + +from pybotx.models.enums import convert_chat_type_from_domain, ChatTypes, APIChatTypes + + +def test__convert_chat_type_from_domain__successful_conversion() -> None: + """Test that convert_chat_type_from_domain successfully converts ChatTypes to APIChatTypes.""" + assert convert_chat_type_from_domain(ChatTypes.PERSONAL_CHAT) == APIChatTypes.CHAT + assert ( + convert_chat_type_from_domain(ChatTypes.GROUP_CHAT) == APIChatTypes.GROUP_CHAT + ) + assert convert_chat_type_from_domain(ChatTypes.CHANNEL) == APIChatTypes.CHANNEL + assert convert_chat_type_from_domain(ChatTypes.THREAD) == APIChatTypes.THREAD + + +def test__convert_chat_type_from_domain__unsupported_chat_type_raises_error() -> None: + """Test that convert_chat_type_from_domain raises NotImplementedError for unsupported chat types.""" + # Create a mock chat type that's not in the mapping + unsupported_chat_type = Mock(spec=ChatTypes) + + # - Act & Assert - + with pytest.raises(NotImplementedError, match="Unsupported chat type"): + convert_chat_type_from_domain(unsupported_chat_type) From e0d8a2753e665f5e83a79cbfdc77be652410aa0c Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Wed, 16 Jul 2025 14:10:19 +0300 Subject: [PATCH 14/18] feat: lint --- tests/client/chats_api/test_create_chat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/client/chats_api/test_create_chat.py b/tests/client/chats_api/test_create_chat.py index ff0eec93..4abca1d9 100644 --- a/tests/client/chats_api/test_create_chat.py +++ b/tests/client/chats_api/test_create_chat.py @@ -169,7 +169,7 @@ def test__create_chat_payload__invalid_avatar_non_data_url_error() -> None: # Test the validator directly since UnverifiedPayloadBaseModel bypasses validation # The validator is a classmethod that expects (cls, value) with pytest.raises(ValueError, match="Avatar must be a data URL \\(RFC2397\\)"): - BotXAPICreateChatRequestPayload._validate_avatar("invalid-url") # type: ignore[call-arg] + BotXAPICreateChatRequestPayload._validate_avatar("invalid-url") def test__create_chat_payload__invalid_avatar_bad_rfc2397_format_error() -> None: @@ -178,7 +178,7 @@ def test__create_chat_payload__invalid_avatar_bad_rfc2397_format_error() -> None # Test the validator directly since UnverifiedPayloadBaseModel bypasses validation with pytest.raises(ValueError, match="Invalid data URL format"): - BotXAPICreateChatRequestPayload._validate_avatar("data:invalid-format") # type: ignore[call-arg] + BotXAPICreateChatRequestPayload._validate_avatar("data:invalid-format") def test__create_chat_payload__avatar_validator_with_none() -> None: @@ -186,7 +186,7 @@ def test__create_chat_payload__avatar_validator_with_none() -> None: from pybotx.client.chats_api.create_chat import BotXAPICreateChatRequestPayload # Test the validator directly with None value - result = BotXAPICreateChatRequestPayload._validate_avatar(None) # type: ignore[call-arg] + result = BotXAPICreateChatRequestPayload._validate_avatar(None) assert result is None @@ -195,7 +195,7 @@ def test__create_chat_payload__avatar_validator_with_valid_data_url() -> None: from pybotx.client.chats_api.create_chat import BotXAPICreateChatRequestPayload valid_avatar = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" - result = BotXAPICreateChatRequestPayload._validate_avatar(valid_avatar) # type: ignore[call-arg] + result = BotXAPICreateChatRequestPayload._validate_avatar(valid_avatar) assert result == valid_avatar From 102bd264ef142f0580dd6d749d04462f7a92d029 Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Wed, 16 Jul 2025 14:24:30 +0300 Subject: [PATCH 15/18] feat: lint --- pybotx/async_buffer.py | 2 +- pybotx/bot/middlewares/exception_middleware.py | 2 +- pybotx/client/exceptions/base.py | 2 +- pybotx/client/get_token.py | 2 +- pybotx/client/stickers_api/add_sticker.py | 2 +- pybotx/constants.py | 2 +- pybotx/models/attachments.py | 2 +- pybotx/models/enums.py | 10 +++++----- pybotx/models/system_events/smartapp_event.py | 4 ++-- tests/client/users_api/convert_to_datetime.py | 2 +- tests/client/users_api/test_users_as_csv.py | 2 +- tests/conftest.py | 4 ++-- tests/test_handler_collector.py | 2 +- tests/test_middlewares.py | 8 ++++---- 14 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pybotx/async_buffer.py b/pybotx/async_buffer.py index 37ab6b69..1dc4aa13 100644 --- a/pybotx/async_buffer.py +++ b/pybotx/async_buffer.py @@ -5,7 +5,7 @@ try: from typing import Protocol except ImportError: - from typing_extensions import Protocol # type: ignore # noqa: WPS440 + from typing_extensions import Protocol # type: ignore class AsyncBufferBase(Protocol): diff --git a/pybotx/bot/middlewares/exception_middleware.py b/pybotx/bot/middlewares/exception_middleware.py index 53317780..c7ac4d50 100644 --- a/pybotx/bot/middlewares/exception_middleware.py +++ b/pybotx/bot/middlewares/exception_middleware.py @@ -33,7 +33,7 @@ async def dispatch( if exception_handler is None: raise message_handler_exc - try: # noqa: WPS505 + try: await exception_handler(message, bot, message_handler_exc) except Exception as error_handler_exc: exc_name = type(message_handler_exc).__name__ diff --git a/pybotx/client/exceptions/base.py b/pybotx/client/exceptions/base.py index 9bd8de1d..17a21c5b 100644 --- a/pybotx/client/exceptions/base.py +++ b/pybotx/client/exceptions/base.py @@ -22,7 +22,7 @@ def from_response( content = response.content message = ( - f"{method} {url}\n" # noqa: WPS221 (Strange error on CI) + f"{method} {url}\n" # (Strange error on CI) f"failed with code {status_code} and payload:\n" f"{content!r}" ) diff --git a/pybotx/client/get_token.py b/pybotx/client/get_token.py index 1a96fda4..cf331c24 100644 --- a/pybotx/client/get_token.py +++ b/pybotx/client/get_token.py @@ -13,7 +13,7 @@ async def get_token( bot_id: UUID, httpx_client: httpx.AsyncClient, bot_accounts_storage: BotAccountsStorage, -) -> str: # noqa: DAR101, DAR201 +) -> str: """Request token for bot. Moved to separate file because used in `AuthorizedBotXMethod` and `Bot.get_token`. diff --git a/pybotx/client/stickers_api/add_sticker.py b/pybotx/client/stickers_api/add_sticker.py index 3b67e4bd..76fef6a3 100644 --- a/pybotx/client/stickers_api/add_sticker.py +++ b/pybotx/client/stickers_api/add_sticker.py @@ -55,7 +55,7 @@ def to_domain(self, pack_id: UUID) -> Sticker: ) -def bad_request_error_handler(response: httpx.Response) -> NoReturn: # noqa: WPS238 +def bad_request_error_handler(response: httpx.Response) -> NoReturn: reason = response.json().get("reason") if reason == "pack_not_found": diff --git a/pybotx/constants.py b/pybotx/constants.py index 39d40a25..6e95c04a 100644 --- a/pybotx/constants.py +++ b/pybotx/constants.py @@ -1,7 +1,7 @@ try: from typing import Final except ImportError: - from typing_extensions import Final # type: ignore # noqa: WPS440 + from typing_extensions import Final # type: ignore CHUNK_SIZE: Final = 1024 * 1024 # 1Mb BOT_API_VERSION: Final = 4 diff --git a/pybotx/models/attachments.py b/pybotx/models/attachments.py index 22c6d4a9..2f7d6365 100644 --- a/pybotx/models/attachments.py +++ b/pybotx/models/attachments.py @@ -213,7 +213,7 @@ class BotAPIAttachmentLink(VerifiedPayloadBaseModel): ] -def convert_api_attachment_to_domain( # noqa: WPS212 +def convert_api_attachment_to_domain( api_attachment: BotAPIAttachment, message_body: str, ) -> IncomingAttachment: diff --git a/pybotx/models/enums.py b/pybotx/models/enums.py index 1a4bc0e0..4a672c37 100644 --- a/pybotx/models/enums.py +++ b/pybotx/models/enums.py @@ -83,7 +83,7 @@ class ConferenceLinkTypes(AutoName): IncomingSyncSourceTypes = Union[SyncSourceTypes, UNSUPPORTED] -class StrEnum(str, Enum): # noqa: WPS600 (pydantic needs this inheritance) +class StrEnum(str, Enum): # (pydantic needs this inheritance) """Enum base for API models.""" # https://github.com/pydantic/pydantic/issues/3850 @@ -311,13 +311,13 @@ def convert_chat_type_from_domain(chat_type: ChatTypes) -> APIChatTypes: @overload def convert_chat_type_to_domain( chat_type: APIChatTypes, -) -> ChatTypes: ... # noqa: WPS428, E704 +) -> ChatTypes: ... @overload def convert_chat_type_to_domain( chat_type: str, -) -> UNSUPPORTED: ... # noqa: WPS428, E704 +) -> UNSUPPORTED: ... def convert_chat_type_to_domain( @@ -345,13 +345,13 @@ def convert_chat_type_to_domain( @overload def convert_sync_source_type_to_domain( sync_type: APISyncSourceTypes, -) -> SyncSourceTypes: ... # noqa: WPS428, E704 +) -> SyncSourceTypes: ... @overload def convert_sync_source_type_to_domain( sync_type: str, -) -> UNSUPPORTED: ... # noqa: WPS428, E704 +) -> UNSUPPORTED: ... def convert_sync_source_type_to_domain( diff --git a/pybotx/models/system_events/smartapp_event.py b/pybotx/models/system_events/smartapp_event.py index 3fd5cf25..c7e8d5e2 100644 --- a/pybotx/models/system_events/smartapp_event.py +++ b/pybotx/models/system_events/smartapp_event.py @@ -38,7 +38,7 @@ class SmartAppEvent(BotCommandBase): ref: Optional[UUID] smartapp_id: UUID - data: Dict[str, Any] # noqa: WPS110 + data: Dict[str, Any] opts: Optional[Dict[str, Any]] smartapp_api_version: Optional[int] files: List[File] @@ -49,7 +49,7 @@ class SmartAppEvent(BotCommandBase): class BotAPISmartAppData(VerifiedPayloadBaseModel): ref: UUID smartapp_id: UUID - data: Dict[str, Any] # noqa: WPS110 + data: Dict[str, Any] opts: Dict[str, Any] smartapp_api_version: int diff --git a/tests/client/users_api/convert_to_datetime.py b/tests/client/users_api/convert_to_datetime.py index 788bd108..a71979c5 100644 --- a/tests/client/users_api/convert_to_datetime.py +++ b/tests/client/users_api/convert_to_datetime.py @@ -4,6 +4,6 @@ def convert_to_datetime(str_datetime: str) -> datetime: datetime_instance = datetime.strptime( str_datetime, - "%Y-%m-%dT%H:%M:%S.%fZ", # noqa: WPS325 + "%Y-%m-%dT%H:%M:%S.%fZ", ) return datetime_instance.replace(tzinfo=timezone.utc) diff --git a/tests/client/users_api/test_users_as_csv.py b/tests/client/users_api/test_users_as_csv.py index 57d776cd..200b62c0 100644 --- a/tests/client/users_api/test_users_as_csv.py +++ b/tests/client/users_api/test_users_as_csv.py @@ -45,7 +45,7 @@ async def test__users_as_csv__no_user_kind_selected_error( # - Act - with pytest.raises(NoUserKindSelectedError) as exc: async with lifespan_wrapper(built_bot) as bot: - async with bot.users_as_csv( # noqa: WPS328 + async with bot.users_as_csv( bot_id=bot_id, cts_user=False, unregistered=False, diff --git a/tests/conftest.py b/tests/conftest.py index db6ac474..b2755b68 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,7 +47,7 @@ def prepared_bot_accounts_storage( @pytest.fixture def datetime_formatter() -> Callable[[str], datetime]: - class DateTimeFormatter(BaseModel): # noqa: WPS431 + class DateTimeFormatter(BaseModel): value: datetime def factory(dt_str: str) -> datetime: @@ -149,7 +149,7 @@ def loguru_caplog( ) -> Generator[pytest.LogCaptureFixture, None, None]: # https://github.com/Delgan/loguru/issues/59 - class PropogateHandler(logging.Handler): # noqa: WPS431 + class PropogateHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: logging.getLogger(record.name).handle(record) diff --git a/tests/test_handler_collector.py b/tests/test_handler_collector.py index 727227f0..a9d1d48e 100644 --- a/tests/test_handler_collector.py +++ b/tests/test_handler_collector.py @@ -562,6 +562,6 @@ async def test__handler_collector__sync_smartapp_event__decorator__handler_alrea @collector.sync_smartapp_event async def duplicated_handle_sync_smartapp_event( *_: Any, - ) -> Any: ... # noqa: E704 + ) -> Any: ... assert str(exc.value) == "Handler for sync smartapp event already registered" diff --git a/tests/test_middlewares.py b/tests/test_middlewares.py index f573051b..4c2b42dd 100644 --- a/tests/test_middlewares.py +++ b/tests/test_middlewares.py @@ -35,7 +35,7 @@ async def middleware( bot: Bot, call_next: IncomingMessageHandlerFunc, ) -> None: - middlewares_called_order.append(number) # noqa: F824 + middlewares_called_order.append(number) await call_next(message, bot) @@ -83,7 +83,7 @@ async def middleware( bot: Bot, call_next: IncomingMessageHandlerFunc, ) -> None: - middlewares_called_order.append(number) # noqa: F824 + middlewares_called_order.append(number) await call_next(message, bot) @@ -127,7 +127,7 @@ async def middleware( bot: Bot, call_next: IncomingMessageHandlerFunc, ) -> None: - middlewares_called_order.append(number) # noqa: F824 + middlewares_called_order.append(number) await call_next(message, bot) @@ -174,7 +174,7 @@ async def middleware( bot: Bot, call_next: IncomingMessageHandlerFunc, ) -> None: - nonlocal middlewares_called_order # noqa: F824 + nonlocal middlewares_called_order middlewares_called_order.append(number) await call_next(message, bot) From 8b7ebd6e0cb993d1d3bdc0da39ffc69986740d82 Mon Sep 17 00:00:00 2001 From: dmitrymihalev Date: Thu, 31 Jul 2025 15:24:23 +0500 Subject: [PATCH 16/18] feat: Add method to create thread to Bot * feat: Add method to create thread to Bot * fix: linter errors * fix: remove duplicate exception * fix: docstring format * add enum value for thread chat type * Add test cases, move duplicating code to fixture, fix name of test --- pybotx/__init__.py | 5 + pybotx/bot/bot.py | 31 ++- pybotx/client/chats_api/create_thread.py | 57 ++++ pybotx/client/exceptions/chats.py | 21 ++ pybotx/models/enums.py | 4 + pyproject.toml | 4 +- tests/client/chats_api/test_create_thread.py | 261 +++++++++++++++++++ 7 files changed, 376 insertions(+), 7 deletions(-) create mode 100644 pybotx/client/chats_api/create_thread.py create mode 100644 tests/client/chats_api/test_create_thread.py diff --git a/pybotx/__init__.py b/pybotx/__init__.py index e14e9439..456feb01 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -39,6 +39,8 @@ ChatCreationError, ChatCreationProhibitedError, InvalidUsersListError, + ThreadCreationError, + ThreadCreationProhibitedError, ) from pybotx.client.exceptions.common import ( ChatNotFoundError, @@ -260,6 +262,9 @@ "SyncSmartAppEventHandlerFunc", "SyncSmartAppEventHandlerNotFoundError", "SyncSourceTypes", + "ThreadCreationError", + "ThreadCreationEventNotFoundError", + "ThreadCreationProhibitedError", "UnknownBotAccountError", "UnknownSystemEventError", "UnsupportedBotAPIVersionError", diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 0cd21e17..887a0ee7 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -1,17 +1,13 @@ from asyncio import Task +from collections.abc import AsyncIterable, AsyncIterator, Iterator, Mapping, Sequence from contextlib import asynccontextmanager from datetime import datetime from types import SimpleNamespace from typing import ( Any, - AsyncIterable, - AsyncIterator, Dict, - Iterator, List, - Mapping, Optional, - Sequence, Set, Tuple, Union, @@ -57,6 +53,10 @@ BotXAPICreateChatRequestPayload, CreateChatMethod, ) +from pybotx.client.chats_api.create_thread import ( + BotXAPICreateThreadRequestPayload, + CreateThreadMethod, +) from pybotx.client.chats_api.disable_stealth import ( BotXAPIDisableStealthRequestPayload, DisableStealthMethod, @@ -1176,6 +1176,27 @@ async def create_chat( return botx_api_chat_id.to_domain() + async def create_thread(self, bot_id: UUID, sync_id: UUID) -> UUID: + """ + Create thread. + + :param bot_id: Bot which should perform the request. + :param sync_id: Message for which thread should be created + + :return: Created thread uuid. + """ + + method = CreateThreadMethod( + bot_id, + self._httpx_client, + self._bot_accounts_storage, + ) + + payload = BotXAPICreateThreadRequestPayload.from_domain(sync_id=sync_id) + botx_api_thread_id = await method.execute(payload) + + return botx_api_thread_id.to_domain() + async def pin_message( self, *, diff --git a/pybotx/client/chats_api/create_thread.py b/pybotx/client/chats_api/create_thread.py new file mode 100644 index 00000000..f7370b41 --- /dev/null +++ b/pybotx/client/chats_api/create_thread.py @@ -0,0 +1,57 @@ +from typing import Literal +from uuid import UUID + +from pybotx.client.authorized_botx_method import AuthorizedBotXMethod +from pybotx.client.botx_method import response_exception_thrower +from pybotx.client.exceptions.chats import ( + ThreadCreationError, + ThreadCreationProhibitedError, +) +from pybotx.client.exceptions.event import EventNotFoundError +from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel + + +class BotXAPICreateThreadRequestPayload(UnverifiedPayloadBaseModel): + sync_id: UUID + + @classmethod + def from_domain(cls, sync_id: UUID) -> "BotXAPICreateThreadRequestPayload": + return cls(sync_id=sync_id) + + +class BotXAPIThreadIdResult(VerifiedPayloadBaseModel): + thread_id: UUID + + +class BotXAPICreateThreadResponsePayload(VerifiedPayloadBaseModel): + status: Literal["ok"] + result: BotXAPIThreadIdResult + + def to_domain(self) -> UUID: + return self.result.thread_id + + +class CreateThreadMethod(AuthorizedBotXMethod): + status_handlers = { + **AuthorizedBotXMethod.status_handlers, + 403: response_exception_thrower(ThreadCreationProhibitedError), + 404: response_exception_thrower(EventNotFoundError), + 422: response_exception_thrower(ThreadCreationError), + } + + async def execute( + self, + payload: BotXAPICreateThreadRequestPayload, + ) -> BotXAPICreateThreadResponsePayload: + path = "/api/v3/botx/chats/create_thread" + + response = await self._botx_method_call( + "POST", + self._build_url(path), + json=payload.jsonable_dict(), + ) + + return self._verify_and_extract_api_model( + BotXAPICreateThreadResponsePayload, + response, + ) diff --git a/pybotx/client/exceptions/chats.py b/pybotx/client/exceptions/chats.py index 4134b5a0..50a2d3a1 100644 --- a/pybotx/client/exceptions/chats.py +++ b/pybotx/client/exceptions/chats.py @@ -15,3 +15,24 @@ class ChatCreationProhibitedError(BaseClientError): class ChatCreationError(BaseClientError): """Error while chat creation.""" + + +class ThreadCreationError(BaseClientError): + """Error while thread creation (invalid scheme).""" + + +class ThreadCreationProhibitedError(BaseClientError): + """ + Error while permission checks. + + 1. Bot has no permissions to create thread + 2. Threads are not allowed for that message + 3. Bot is not a chat member where message is located + 4. Message is located in personal chat + 5. Usupported event type + 6. Unsuppoerted chat type + 7. Thread is already created + 8. No access for message + 9. Message in stealth mode + 10. Message is deleted + """ diff --git a/pybotx/models/enums.py b/pybotx/models/enums.py index 05bc35fa..d56f61d7 100644 --- a/pybotx/models/enums.py +++ b/pybotx/models/enums.py @@ -59,6 +59,7 @@ class ChatTypes(AutoName): PERSONAL_CHAT = auto() GROUP_CHAT = auto() CHANNEL = auto() + THREAD = auto() class SyncSourceTypes(AutoName): @@ -92,6 +93,7 @@ class APIChatTypes(Enum): CHAT = "chat" GROUP_CHAT = "group_chat" CHANNEL = "channel" + THREAD = "thread" class BotAPICommandTypes(StrEnum): @@ -295,6 +297,7 @@ def convert_chat_type_from_domain(chat_type: ChatTypes) -> APIChatTypes: ChatTypes.PERSONAL_CHAT: APIChatTypes.CHAT, ChatTypes.GROUP_CHAT: APIChatTypes.GROUP_CHAT, ChatTypes.CHANNEL: APIChatTypes.CHANNEL, + ChatTypes.THREAD: APIChatTypes.THREAD, } converted_type = chat_types_mapping.get(chat_type) @@ -323,6 +326,7 @@ def convert_chat_type_to_domain( APIChatTypes.CHAT: ChatTypes.PERSONAL_CHAT, APIChatTypes.GROUP_CHAT: ChatTypes.GROUP_CHAT, APIChatTypes.CHANNEL: ChatTypes.CHANNEL, + APIChatTypes.THREAD: ChatTypes.THREAD, } converted_type: Optional[IncomingChatTypes] diff --git a/pyproject.toml b/pyproject.toml index 6bb84255..bfdfe41e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [tool.poetry] name = "pybotx" -version = "0.75.1" +version = "0.75.2" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", "Maxim Gorbachev ", "Alexander Samoylenko ", - "Arseniy Zhiltsov " + "Arseniy Zhiltsov ", ] readme = "README.md" repository = "https://github.com/ExpressApp/pybotx" diff --git a/tests/client/chats_api/test_create_thread.py b/tests/client/chats_api/test_create_thread.py new file mode 100644 index 00000000..bfb56fbd --- /dev/null +++ b/tests/client/chats_api/test_create_thread.py @@ -0,0 +1,261 @@ +from http import HTTPStatus +from typing import Any, Callable +from uuid import UUID + +import httpx +import pytest +from respx import Route +from respx.router import MockRouter + +from pybotx import ( + Bot, + BotAccountWithSecret, + EventNotFoundError, + HandlerCollector, + ThreadCreationError, + ThreadCreationProhibitedError, + lifespan_wrapper, +) + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + +ENDPOINT = "api/v3/botx/chats/create_thread" + + +@pytest.fixture +def sync_id() -> str: + return "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3" + + +@pytest.fixture +def create_mocked_endpoint( + respx_mock: MockRouter, + host: str, + sync_id: str, +) -> Callable[[dict[str, Any], int], Route]: + def mocked_endpoint(json_response: dict[str, Any], status_code: int) -> Route: + return respx_mock.post( + f"https://{host}/{ENDPOINT}", + headers={ + "Authorization": "Bearer token", + "Content-Type": "application/json", + }, + json={"sync_id": sync_id}, + ).mock(return_value=httpx.Response(status_code, json=json_response)) + + return mocked_endpoint + + +async def test__create_thread__succeed( + create_mocked_endpoint: Callable[[dict[str, Any], int], Route], + sync_id: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + thread_id = "2a8c0d1e-c4d1-4308-b024-6e1a9f4a4b6d" + endpoint = create_mocked_endpoint( + { + "status": "ok", + "result": {"thread_id": thread_id}, + }, + HTTPStatus.OK, + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + created_thread_id = await bot.create_thread( + bot_id=bot_id, + sync_id=UUID(sync_id), + ) + + # - Assert - + assert str(created_thread_id) == thread_id + assert endpoint.called + + +@pytest.mark.parametrize( + "return_json, response_status, expected_exc_type", + ( + ( + { + "status": "error", + "reason": "thread_creation_is_prohibited", + "errors": ["This bot is not allowed to create thread"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "threads_not_enabled", + "errors": ["Threads not enabled for this chat"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "bot_is_not_a_chat_member", + "errors": ["This bot is not a chat member"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "can_not_create_for_personal_chat", + "errors": ["This is personal chat"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "unsupported_event_type", + "errors": ["This event type is unsupported"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "unsupported_chat_type", + "errors": ["This chat type is unsupported"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "thread_already_created", + "errors": ["Thread already created"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "no_access_for_message", + "errors": ["There is no access for this message"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "event_in_stealth_mode", + "errors": ["This event is in stealth mode"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "event_already_deleted", + "errors": ["This event already deleted"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "event_not_found", + "errors": ["Event not found"], + "error_data": {"bot_id": "24348246-6791-4ac0-9d86-b948cd6a0e46"}, + }, + HTTPStatus.NOT_FOUND, + EventNotFoundError, + ), + ( + { + "status": "error", + "reason": "|specified reason|", + "errors": ["|specified errors|"], + "error_data": {}, + }, + HTTPStatus.UNPROCESSABLE_ENTITY, + ThreadCreationError, + ), + ( + { + "status": "error", + "reason": None, + "errors": [], + "error_data": {}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "errors": [], + "error_data": {}, + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ( + { + "status": "error", + "reason": "unexpected reason", + "errors": [], + }, + HTTPStatus.FORBIDDEN, + ThreadCreationProhibitedError, + ), + ), +) +async def test__create_thread__botx_error_raised( + create_mocked_endpoint: Callable[[dict[str, Any], int], Route], + sync_id: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, + return_json: dict[str, Any], + response_status: int, + expected_exc_type: type[BaseException], +) -> None: + # - Arrange - + endpoint = create_mocked_endpoint(return_json, response_status) + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + with pytest.raises(expected_exc_type) as exc: + await bot.create_thread( + bot_id=bot_id, + sync_id=UUID(sync_id), + ) + + # - Assert - + assert endpoint.called + + if return_json.get("reason"): + assert return_json["reason"] in str(exc.value) From 6459d7a46bce2c4774b7916d7bd769ad549c36fb Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Thu, 7 Aug 2025 12:15:06 +0300 Subject: [PATCH 17/18] some test ignore --- pybotx/models/enums.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pybotx/models/enums.py b/pybotx/models/enums.py index 4a672c37..7611e695 100644 --- a/pybotx/models/enums.py +++ b/pybotx/models/enums.py @@ -309,13 +309,13 @@ def convert_chat_type_from_domain(chat_type: ChatTypes) -> APIChatTypes: @overload -def convert_chat_type_to_domain( +def convert_chat_type_to_domain( # pragma: no cover chat_type: APIChatTypes, ) -> ChatTypes: ... @overload -def convert_chat_type_to_domain( +def convert_chat_type_to_domain( # pragma: no cover chat_type: str, ) -> UNSUPPORTED: ... @@ -343,13 +343,13 @@ def convert_chat_type_to_domain( @overload -def convert_sync_source_type_to_domain( +def convert_sync_source_type_to_domain( # pragma: no cover sync_type: APISyncSourceTypes, ) -> SyncSourceTypes: ... @overload -def convert_sync_source_type_to_domain( +def convert_sync_source_type_to_domain( # pragma: no cover sync_type: str, ) -> UNSUPPORTED: ... From 589afb6c9673cde7b3f784673380bbe5529cbe7f Mon Sep 17 00:00:00 2001 From: Aleksandr Osovskii Date: Fri, 8 Aug 2025 13:56:02 +0300 Subject: [PATCH 18/18] version up --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cfa84ba6..22e01d4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.75.2" +version = "0.76.0a1" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ",