diff --git a/.github/workflows/test_integration.yml b/.github/workflows/test_integration.yml index 5dd779a..4504da5 100644 --- a/.github/workflows/test_integration.yml +++ b/.github/workflows/test_integration.yml @@ -78,6 +78,7 @@ jobs: else echo "CREDENTIALS_JSON=${{ secrets.PYTEST_CREDENTIALS_LINUX }}" >> $GITHUB_ENV fi + echo "NGROK_AUTH_TOKEN=${{ secrets.PYTEST_NGROK_AUTH_TOKEN }}" >> $GITHUB_ENV #----------------------------------------------- # ------------- run test suite ------------ @@ -86,6 +87,7 @@ jobs: if: always() run: | echo $CREDENTIALS_JSON | base64 -d > ./tests/credentials/test_creds.json + echo "NGROK_AUTH_TOKEN=$NGROK_AUTH_TOKEN" > ./tests/credentials/.env make test #---------------------------------------------- diff --git a/Makefile b/Makefile index 8ec12f8..13c490d 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,12 @@ test: $(MAKE) test-integration @echo "Tests complete!" +### Commands to run the docs ### +run-docs: + @echo "Building documentation..." + @poetry run mkdocs serve + @echo "Documentation built!" + ### Commands to git commit ### stage: @echo "Staging changes..." diff --git a/README.md b/README.md index 0a25c5a..b728f55 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,7 @@ The wrapper currently supports the following APIs: - [x] Delete a webhook - [x] Rotate a webhook signing secret - [x] Retrieve a list of failed webhook events + - [x] Verify a webhook signature ## **Contributing** diff --git a/docs/code_reference/exceptions/exceptions.md b/docs/code_reference/exceptions/exceptions.md index 92fa2df..80db88a 100644 --- a/docs/code_reference/exceptions/exceptions.md +++ b/docs/code_reference/exceptions/exceptions.md @@ -4,18 +4,60 @@ PyRevolut provides a number of exceptions that can be raised during the executio --- -::: pyrevolut.exceptions.common.PyRevolutAPIException +::: pyrevolut.exceptions.common.PyRevolutBaseException --- -::: pyrevolut.exceptions.bad_request.BadRequestException +::: pyrevolut.exceptions.bad_request.PyRevolutBadRequest --- -::: pyrevolut.exceptions.internal_revolut_error.InternalRevolutError +::: pyrevolut.exceptions.conflict.PyRevolutConflict --- -::: pyrevolut.exceptions.invalid_environment.InvalidEnvironmentException +::: pyrevolut.exceptions.forbidden.PyRevolutForbidden --- + +::: pyrevolut.exceptions.internal_server_error.PyRevolutInternalServerError + +--- + +::: pyrevolut.exceptions.invalid_environment.PyRevolutInvalidEnvironment + +--- + +::: pyrevolut.exceptions.invalid_payload.PyRevolutInvalidPayload + +--- + +::: pyrevolut.exceptions.method_not_allowed.PyRevolutMethodNotAllowed + +--- + +::: pyrevolut.exceptions.network_error.PyRevolutNetworkError + +--- + +::: pyrevolut.exceptions.not_acceptable.PyRevolutNotAcceptable + +--- + +::: pyrevolut.exceptions.not_found.PyRevolutNotFound + +--- + +::: pyrevolut.exceptions.server_unavailable.PyRevolutServerUnavailable + +--- + +::: pyrevolut.exceptions.timeout_error.PyRevolutTimeoutError + +--- + +::: pyrevolut.exceptions.too_many_requests.PyRevolutTooManyRequests + +--- + +::: pyrevolut.exceptions.unauthorized.PyRevolutUnauthorized diff --git a/poetry.lock b/poetry.lock index eaf3c21..6470e69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -148,13 +148,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -399,63 +399,63 @@ tomlkit = ">=0.5.3,<1.0.0" [[package]] name = "coverage" -version = "7.5.2" +version = "7.5.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:554c7327bf0fd688050348e22db7c8e163fb7219f3ecdd4732d7ed606b417263"}, - {file = "coverage-7.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d0305e02e40c7cfea5d08d6368576537a74c0eea62b77633179748d3519d6705"}, - {file = "coverage-7.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:829fb55ad437d757c70d5b1c51cfda9377f31506a0a3f3ac282bc6a387d6a5f1"}, - {file = "coverage-7.5.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:894b1acded706f1407a662d08e026bfd0ff1e59e9bd32062fea9d862564cfb65"}, - {file = "coverage-7.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe76d6dee5e4febefa83998b17926df3a04e5089e3d2b1688c74a9157798d7a2"}, - {file = "coverage-7.5.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c7ebf2a37e4f5fea3c1a11e1f47cea7d75d0f2d8ef69635ddbd5c927083211fc"}, - {file = "coverage-7.5.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20e611fc36e1a0fc7bbf957ef9c635c8807d71fbe5643e51b2769b3cc0fb0b51"}, - {file = "coverage-7.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c5c5b7ae2763533152880d5b5b451acbc1089ade2336b710a24b2b0f5239d20"}, - {file = "coverage-7.5.2-cp310-cp310-win32.whl", hash = "sha256:1e4225990a87df898e40ca31c9e830c15c2c53b1d33df592bc8ef314d71f0281"}, - {file = "coverage-7.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:976cd92d9420e6e2aa6ce6a9d61f2b490e07cb468968adf371546b33b829284b"}, - {file = "coverage-7.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5997d418c219dcd4dcba64e50671cca849aaf0dac3d7a2eeeb7d651a5bd735b8"}, - {file = "coverage-7.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec27e93bbf5976f0465e8936f02eb5add99bbe4e4e7b233607e4d7622912d68d"}, - {file = "coverage-7.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f11f98753800eb1ec872562a398081f6695f91cd01ce39819e36621003ec52a"}, - {file = "coverage-7.5.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e34680049eecb30b6498784c9637c1c74277dcb1db75649a152f8004fbd6646"}, - {file = "coverage-7.5.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e12536446ad4527ac8ed91d8a607813085683bcce27af69e3b31cd72b3c5960"}, - {file = "coverage-7.5.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3d3f7744b8a8079d69af69d512e5abed4fb473057625588ce126088e50d05493"}, - {file = "coverage-7.5.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:431a3917e32223fcdb90b79fe60185864a9109631ebc05f6c5aa03781a00b513"}, - {file = "coverage-7.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a7c6574225f34ce45466f04751d957b5c5e6b69fca9351db017c9249786172ce"}, - {file = "coverage-7.5.2-cp311-cp311-win32.whl", hash = "sha256:2b144d142ec9987276aeff1326edbc0df8ba4afbd7232f0ca10ad57a115e95b6"}, - {file = "coverage-7.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:900532713115ac58bc3491b9d2b52704a05ed408ba0918d57fd72c94bc47fba1"}, - {file = "coverage-7.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9a42970ce74c88bdf144df11c52c5cf4ad610d860de87c0883385a1c9d9fa4ab"}, - {file = "coverage-7.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26716a1118c6ce2188283b4b60a898c3be29b480acbd0a91446ced4fe4e780d8"}, - {file = "coverage-7.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60b66b0363c5a2a79fba3d1cd7430c25bbd92c923d031cae906bdcb6e054d9a2"}, - {file = "coverage-7.5.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d22eba19273b2069e4efeff88c897a26bdc64633cbe0357a198f92dca94268"}, - {file = "coverage-7.5.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bb5b92a0ab3d22dfdbfe845e2fef92717b067bdf41a5b68c7e3e857c0cff1a4"}, - {file = "coverage-7.5.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1aef719b6559b521ae913ddeb38f5048c6d1a3d366865e8b320270b7bc4693c2"}, - {file = "coverage-7.5.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8809c0ea0e8454f756e3bd5c36d04dddf222989216788a25bfd6724bfcee342c"}, - {file = "coverage-7.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1acc2e2ef098a1d4bf535758085f508097316d738101a97c3f996bccba963ea5"}, - {file = "coverage-7.5.2-cp312-cp312-win32.whl", hash = "sha256:97de509043d3f0f2b2cd171bdccf408f175c7f7a99d36d566b1ae4dd84107985"}, - {file = "coverage-7.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:8941e35a0e991a7a20a1fa3e3182f82abe357211f2c335a9e6007067c3392fcf"}, - {file = "coverage-7.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5662bf0f6fb6757f5c2d6279c541a5af55a39772c2362ed0920b27e3ce0e21f7"}, - {file = "coverage-7.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d9c62cff2ffb4c2a95328488fd7aa96a7a4b34873150650fe76b19c08c9c792"}, - {file = "coverage-7.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74eeaa13e8200ad72fca9c5f37395fb310915cec6f1682b21375e84fd9770e84"}, - {file = "coverage-7.5.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f29bf497d51a5077994b265e976d78b09d9d0dff6ca5763dbb4804534a5d380"}, - {file = "coverage-7.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f96aa94739593ae0707eda9813ce363a0a0374a810ae0eced383340fc4a1f73"}, - {file = "coverage-7.5.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:51b6cee539168a912b4b3b040e4042b9e2c9a7ad9c8546c09e4eaeff3eacba6b"}, - {file = "coverage-7.5.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:59a75e6aa5c25b50b5a1499f9718f2edff54257f545718c4fb100f48d570ead4"}, - {file = "coverage-7.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29da75ce20cb0a26d60e22658dd3230713c6c05a3465dd8ad040ffc991aea318"}, - {file = "coverage-7.5.2-cp38-cp38-win32.whl", hash = "sha256:23f2f16958b16152b43a39a5ecf4705757ddd284b3b17a77da3a62aef9c057ef"}, - {file = "coverage-7.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:9e41c94035e5cdb362beed681b58a707e8dc29ea446ea1713d92afeded9d1ddd"}, - {file = "coverage-7.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06d96b9b19bbe7f049c2be3c4f9e06737ec6d8ef8933c7c3a4c557ef07936e46"}, - {file = "coverage-7.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:878243e1206828908a6b4a9ca7b1aa8bee9eb129bf7186fc381d2646f4524ce9"}, - {file = "coverage-7.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:482df956b055d3009d10fce81af6ffab28215d7ed6ad4a15e5c8e67cb7c5251c"}, - {file = "coverage-7.5.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a35c97af60a5492e9e89f8b7153fe24eadfd61cb3a2fb600df1a25b5dab34b7e"}, - {file = "coverage-7.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24bb4c7859a3f757a116521d4d3a8a82befad56ea1bdacd17d6aafd113b0071e"}, - {file = "coverage-7.5.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1046aab24c48c694f0793f669ac49ea68acde6a0798ac5388abe0a5615b5ec8"}, - {file = "coverage-7.5.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:448ec61ea9ea7916d5579939362509145caaecf03161f6f13e366aebb692a631"}, - {file = "coverage-7.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4a00bd5ba8f1a4114720bef283cf31583d6cb1c510ce890a6da6c4268f0070b7"}, - {file = "coverage-7.5.2-cp39-cp39-win32.whl", hash = "sha256:9f805481d5eff2a96bac4da1570ef662bf970f9a16580dc2c169c8c3183fa02b"}, - {file = "coverage-7.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:2c79f058e7bec26b5295d53b8c39ecb623448c74ccc8378631f5cb5c16a7e02c"}, - {file = "coverage-7.5.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:40dbb8e7727560fe8ab65efcddfec1ae25f30ef02e2f2e5d78cfb52a66781ec5"}, - {file = "coverage-7.5.2.tar.gz", hash = "sha256:13017a63b0e499c59b5ba94a8542fb62864ba3016127d1e4ef30d354fc2b00e9"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, + {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, + {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, + {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, + {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, + {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, + {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, + {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, + {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, + {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, + {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, + {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, + {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, + {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, + {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, + {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, + {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, + {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, + {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, + {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, + {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, + {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, + {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, + {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, + {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, + {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, + {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, + {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, ] [package.extras] @@ -642,6 +642,20 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] +[[package]] +name = "faker" +version = "25.3.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-25.3.0-py3-none-any.whl", hash = "sha256:0158d47e955b6ec22134c0a74ebb7ed34fe600896208bafbf1008db831b17f04"}, + {file = "Faker-25.3.0.tar.gz", hash = "sha256:bcbe31eee5ef4bbf87ce36c4eba53c01e2a1d912fde2a4d3528b430d2beb784f"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "filelock" version = "3.14.0" @@ -957,6 +971,50 @@ traitlets = ">=5.3" docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] +[[package]] +name = "litestar" +version = "2.8.3" +description = "Litestar - A production-ready, highly performant, extensible ASGI API Framework" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "litestar-2.8.3-py3-none-any.whl", hash = "sha256:bef5e8592b1afba24306f9a5834d84cef89826b12f58a97d3099cf49e2f3767d"}, + {file = "litestar-2.8.3.tar.gz", hash = "sha256:3764eef3885b331bfb15c478a2b9e2bc1980741ff58bd5512d599436f669549c"}, +] + +[package.dependencies] +anyio = ">=3" +click = "*" +httpx = ">=0.22" +msgspec = ">=0.18.2" +multidict = ">=6.0.2" +polyfactory = ">=2.6.3" +pyyaml = "*" +rich = ">=13.0.0" +rich-click = "*" +typing-extensions = "*" + +[package.extras] +annotated-types = ["annotated-types"] +attrs = ["attrs"] +brotli = ["brotli"] +cli = ["jsbeautifier", "uvicorn[standard]", "uvloop (>=0.18.0)"] +cryptography = ["cryptography"] +full = ["advanced-alchemy (>=0.2.2,<0.9.0)", "annotated-types", "attrs", "brotli", "cryptography", "email-validator", "fast-query-parsers (>=1.0.2)", "jinja2", "jinja2 (>=3.1.2)", "jsbeautifier", "mako (>=1.2.4)", "minijinja (>=1.0.0)", "opentelemetry-instrumentation-asgi", "piccolo", "picologging", "prometheus-client", "pydantic", "pydantic-extra-types", "python-jose", "redis[hiredis] (>=4.4.4)", "structlog", "uvicorn[standard]", "uvloop (>=0.18.0)"] +jinja = ["jinja2 (>=3.1.2)"] +jwt = ["cryptography", "python-jose"] +mako = ["mako (>=1.2.4)"] +minijinja = ["minijinja (>=1.0.0)"] +opentelemetry = ["opentelemetry-instrumentation-asgi"] +piccolo = ["piccolo"] +picologging = ["picologging"] +prometheus = ["prometheus-client"] +pydantic = ["email-validator", "pydantic", "pydantic-extra-types"] +redis = ["redis[hiredis] (>=4.4.4)"] +sqlalchemy = ["advanced-alchemy (>=0.2.2,<0.9.0)"] +standard = ["fast-query-parsers (>=1.0.2)", "jinja2", "jsbeautifier", "uvicorn[standard]", "uvloop (>=0.18.0)"] +structlog = ["structlog"] + [[package]] name = "markdown" version = "3.6" @@ -1245,6 +1303,157 @@ files = [ griffe = ">=0.44" mkdocstrings = ">=0.25" +[[package]] +name = "msgspec" +version = "0.18.6" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, + {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, + {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, + {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, + {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, + {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, + {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, +] + +[package.extras] +dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] +doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] +test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] +toml = ["tomli", "tomli-w"] +yaml = ["pyyaml"] + +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -1269,18 +1478,15 @@ files = [ [[package]] name = "nodeenv" -version = "1.8.0" +version = "1.9.0" description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, + {file = "nodeenv-1.9.0-py2.py3-none-any.whl", hash = "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a"}, + {file = "nodeenv-1.9.0.tar.gz", hash = "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1"}, ] -[package.dependencies] -setuptools = "*" - [[package]] name = "packaging" version = "24.0" @@ -1483,6 +1689,30 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "polyfactory" +version = "2.16.0" +description = "Mock data generation factories" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "polyfactory-2.16.0-py3-none-any.whl", hash = "sha256:168d8e50b77e91e35e691e8b3eedac43d7e423a6857fa26d473def96d53f0ecf"}, + {file = "polyfactory-2.16.0.tar.gz", hash = "sha256:03d8c706b70c4782ac8e637d0f6ab52760a7d11b712da5936a95a8f7022b2688"}, +] + +[package.dependencies] +faker = "*" +typing-extensions = ">=4.6.0" + +[package.extras] +attrs = ["attrs (>=22.2.0)"] +beanie = ["beanie", "pydantic[email]"] +full = ["attrs", "beanie", "msgspec", "odmantic", "pydantic", "sqlalchemy"] +msgspec = ["msgspec"] +odmantic = ["odmantic (<1.0.0)", "pydantic[email]"] +pydantic = ["pydantic[email]"] +sqlalchemy = ["sqlalchemy (>=1.4.29)"] + [[package]] name = "pprintpp" version = "0.4.0" @@ -1603,19 +1833,19 @@ files = [ [[package]] name = "pydantic" -version = "2.7.1" +version = "2.7.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, - {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, + {file = "pydantic-2.7.2-py3-none-any.whl", hash = "sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7"}, + {file = "pydantic-2.7.2.tar.gz", hash = "sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7"}, ] [package.dependencies] annotated-types = ">=0.4.0" email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} -pydantic-core = "2.18.2" +pydantic-core = "2.18.3" typing-extensions = ">=4.6.1" [package.extras] @@ -1623,90 +1853,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.18.2" +version = "2.18.3" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, - {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, - {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, - {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, - {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, - {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, - {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, - {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, - {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, - {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, - {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, - {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, - {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, - {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, + {file = "pydantic_core-2.18.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c"}, + {file = "pydantic_core-2.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7"}, + {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c"}, + {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2"}, + {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34"}, + {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a"}, + {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b"}, + {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6"}, + {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426"}, + {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812"}, + {file = "pydantic_core-2.18.3-cp310-none-win32.whl", hash = "sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779"}, + {file = "pydantic_core-2.18.3-cp310-none-win_amd64.whl", hash = "sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0"}, + {file = "pydantic_core-2.18.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab"}, + {file = "pydantic_core-2.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2"}, + {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb"}, + {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231"}, + {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be"}, + {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd"}, + {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106"}, + {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4"}, + {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe"}, + {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d"}, + {file = "pydantic_core-2.18.3-cp311-none-win32.whl", hash = "sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7"}, + {file = "pydantic_core-2.18.3-cp311-none-win_amd64.whl", hash = "sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7"}, + {file = "pydantic_core-2.18.3-cp311-none-win_arm64.whl", hash = "sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4"}, + {file = "pydantic_core-2.18.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f"}, + {file = "pydantic_core-2.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9"}, + {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c"}, + {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7"}, + {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048"}, + {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326"}, + {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4"}, + {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022"}, + {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd"}, + {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be"}, + {file = "pydantic_core-2.18.3-cp312-none-win32.whl", hash = "sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5"}, + {file = "pydantic_core-2.18.3-cp312-none-win_amd64.whl", hash = "sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6"}, + {file = "pydantic_core-2.18.3-cp312-none-win_arm64.whl", hash = "sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417"}, + {file = "pydantic_core-2.18.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522"}, + {file = "pydantic_core-2.18.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702"}, + {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2"}, + {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5"}, + {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361"}, + {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60"}, + {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044"}, + {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc"}, + {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4"}, + {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0"}, + {file = "pydantic_core-2.18.3-cp38-none-win32.whl", hash = "sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558"}, + {file = "pydantic_core-2.18.3-cp38-none-win_amd64.whl", hash = "sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc"}, + {file = "pydantic_core-2.18.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083"}, + {file = "pydantic_core-2.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef"}, + {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1"}, + {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442"}, + {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8"}, + {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9"}, + {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943"}, + {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06"}, + {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6"}, + {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af"}, + {file = "pydantic_core-2.18.3-cp39-none-win32.whl", hash = "sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78"}, + {file = "pydantic_core-2.18.3-cp39-none-win_amd64.whl", hash = "sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb"}, + {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1"}, + {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a"}, + {file = "pydantic_core-2.18.3.tar.gz", hash = "sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39"}, ] [package.dependencies] @@ -1781,6 +2011,24 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] +[[package]] +name = "pyngrok" +version = "7.1.6" +description = "A Python wrapper for ngrok." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyngrok-7.1.6-py3-none-any.whl", hash = "sha256:422ac7c339622fef51308f0c493a1f5a05d0f403eee5bdd183fb4021a6cb90d4"}, + {file = "pyngrok-7.1.6.tar.gz", hash = "sha256:05c0fca6340913658abddc623a0a53927aced93e27ffef801d24814bcb180eaa"}, +] + +[package.dependencies] +PyYAML = ">=5.1" + +[package.extras] +dev = ["coverage[toml]", "flake8", "flake8-pyproject", "pep8-naming", "psutil"] +docs = ["Sphinx", "mypy", "sphinx-autodoc-typehints (==1.25.2)", "sphinx-notfound-page", "sphinx-substitution-extensions", "types-PyYAML"] + [[package]] name = "pytest" version = "7.4.4" @@ -1984,6 +2232,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pywin32" version = "306" @@ -2285,13 +2547,13 @@ files = [ [[package]] name = "requests" -version = "2.32.2" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, - {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -2323,45 +2585,50 @@ pygments = ">=2.13.0,<3.0.0" jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] -name = "ruff" -version = "0.4.5" -description = "An extremely fast Python linter and code formatter, written in Rust." +name = "rich-click" +version = "1.8.2" +description = "Format click help output nicely with rich" optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, - {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, - {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, - {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, - {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, - {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, - {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, - {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, -] - -[[package]] -name = "setuptools" -version = "70.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, + {file = "rich_click-1.8.2-py3-none-any.whl", hash = "sha256:b57856f304e4fe0394b82d7ce0784450758f8c8b4e201ccc4320501cc201806b"}, + {file = "rich_click-1.8.2.tar.gz", hash = "sha256:8e29bdede858b59aa2859a1ab1c4ccbd39ed7ed5870262dae756fba6b5dc72e8"}, ] +[package.dependencies] +click = ">=7" +rich = ">=10.7" +typing-extensions = "*" + [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] +docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] + +[[package]] +name = "ruff" +version = "0.4.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"}, + {file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"}, + {file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"}, + {file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"}, + {file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"}, + {file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"}, + {file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"}, + {file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"}, +] [[package]] name = "shellingham" @@ -2494,13 +2761,13 @@ typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" -version = "4.12.0" +version = "4.12.1" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, - {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, + {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, + {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, ] [[package]] @@ -2531,6 +2798,24 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.30.1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"}, + {file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "virtualenv" version = "20.26.2" @@ -2608,20 +2893,20 @@ files = [ [[package]] name = "zipp" -version = "3.19.0" +version = "3.19.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.19.0-py3-none-any.whl", hash = "sha256:96dc6ad62f1441bcaccef23b274ec471518daf4fbbc580341204936a5a3dddec"}, - {file = "zipp-3.19.0.tar.gz", hash = "sha256:952df858fb3164426c976d9338d3961e8e8b3758e2e059e0f754b8c4262625ee"}, + {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, + {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "4eb8efa6ca86e536fb7be4b56e2e1cd66a7d20e0a538eebec416c90be90d32a9" +content-hash = "2d3e6b30163ffc6dd267b7603a3ae221b715074b6d0797a6cbf279bcde3e006b" diff --git a/pyproject.toml b/pyproject.toml index 2d2b8e9..bd39711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,10 @@ pytest-randomly = "3.*" pytest-clarity = "1.*" pytest-split = "0.*" pytest-env = "1.*" +pyngrok = "7.*" +litestar = "2.*" +uvicorn = "0.*" +python-dotenv = "1.*" [tool.poetry.group.docs.dependencies] mkdocs-material = "9.*" diff --git a/pyrevolut/api/cards/endpoint/asynchronous.py b/pyrevolut/api/cards/endpoint/asynchronous.py index 607062c..5d98d9e 100644 --- a/pyrevolut/api/cards/endpoint/asynchronous.py +++ b/pyrevolut/api/cards/endpoint/asynchronous.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from pyrevolut.utils import DateTime -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment from pyrevolut.api.common import BaseEndpointAsync, EnumMerchantCategory from pyrevolut.api.cards.get import ( RetrieveListOfCards, @@ -595,10 +595,10 @@ def __check_sandbox(self): Raises ------ - InvalidEnvironmentException + PyRevolutInvalidEnvironment If the sandbox is enabled. """ if self.client.sandbox: - raise InvalidEnvironmentException( + raise PyRevolutInvalidEnvironment( "This feature is not available in Sandbox." ) diff --git a/pyrevolut/api/cards/endpoint/synchronous.py b/pyrevolut/api/cards/endpoint/synchronous.py index 25cf9f6..0c4f126 100644 --- a/pyrevolut/api/cards/endpoint/synchronous.py +++ b/pyrevolut/api/cards/endpoint/synchronous.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from pyrevolut.utils import DateTime -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment from pyrevolut.api.common import BaseEndpointSync, EnumMerchantCategory from pyrevolut.api.cards.get import ( RetrieveListOfCards, @@ -595,10 +595,10 @@ def __check_sandbox(self): Raises ------ - InvalidEnvironmentException + PyRevolutInvalidEnvironment If the sandbox is enabled. """ if self.client.sandbox: - raise InvalidEnvironmentException( + raise PyRevolutInvalidEnvironment( "This feature is not available in Sandbox." ) diff --git a/pyrevolut/api/common/enums/__init__.py b/pyrevolut/api/common/enums/__init__.py index 5adbd34..d8baf06 100644 --- a/pyrevolut/api/common/enums/__init__.py +++ b/pyrevolut/api/common/enums/__init__.py @@ -18,7 +18,6 @@ from .profile_state import EnumProfileState from .profile_type import EnumProfileType from .recipient_charges import EnumRecipientCharges -from .simulate_transfer_state_action import EnumSimulateTransferStateAction from .team_member_state import EnumTeamMemberState from .time_unit import EnumTimeUnit from .transaction_type import EnumTransactionType diff --git a/pyrevolut/api/common/enums/simulate_transfer_state_action.py b/pyrevolut/api/common/enums/simulate_transfer_state_action.py deleted file mode 100644 index aa13235..0000000 --- a/pyrevolut/api/common/enums/simulate_transfer_state_action.py +++ /dev/null @@ -1,12 +0,0 @@ -from enum import StrEnum - - -class EnumSimulateTransferStateAction(StrEnum): - """ - Enum for simulate transfer state action - """ - - COMPLETE = "complete" - REVERT = "revert" - DECLINE = "decline" - FAIL = "fail" diff --git a/pyrevolut/api/simulations/endpoint/asynchronous.py b/pyrevolut/api/simulations/endpoint/asynchronous.py index 1f3e4b3..dd2d064 100644 --- a/pyrevolut/api/simulations/endpoint/asynchronous.py +++ b/pyrevolut/api/simulations/endpoint/asynchronous.py @@ -1,10 +1,10 @@ from uuid import UUID +from typing import Literal -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment from pyrevolut.api.common import ( BaseEndpointAsync, EnumTransactionState, - EnumSimulateTransferStateAction, ) from pyrevolut.api.simulations.post import ( SimulateAccountTopup, @@ -92,7 +92,7 @@ async def simulate_account_topup( async def simulate_transfer_state_update( self, transfer_id: UUID, - action: EnumSimulateTransferStateAction, + action: Literal["complete", "revert", "decline", "fail"], **kwargs, ) -> dict | SimulateTransferStateUpdate.Response: """ @@ -107,7 +107,7 @@ async def simulate_transfer_state_update( ---------- transfer_id : UUID The ID of the transfer whose state you want to update. - action : EnumSimulateTransferStateAction + action : Literal["complete", "revert", "decline", "fail"] The action you want to perform on the transfer. Possible values: complete: @@ -125,6 +125,7 @@ async def simulate_transfer_state_update( The updated transfer information. """ self.__check_sandbox() + assert action in ["complete", "revert", "decline", "fail"], "Invalid action" endpoint = SimulateTransferStateUpdate path = endpoint.ROUTE.format(transfer_id=transfer_id, action=action) body = endpoint.Body() @@ -142,10 +143,10 @@ def __check_sandbox(self): Raises ------ - InvalidEnvironmentException + PyRevolutInvalidEnvironment If the sandbox is enabled. """ if not self.client.sandbox: - raise InvalidEnvironmentException( + raise PyRevolutInvalidEnvironment( "This feature is only available in the Sandbox." ) diff --git a/pyrevolut/api/simulations/endpoint/synchronous.py b/pyrevolut/api/simulations/endpoint/synchronous.py index 57182af..9f588ed 100644 --- a/pyrevolut/api/simulations/endpoint/synchronous.py +++ b/pyrevolut/api/simulations/endpoint/synchronous.py @@ -1,10 +1,10 @@ from uuid import UUID +from typing import Literal -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment from pyrevolut.api.common import ( BaseEndpointSync, EnumTransactionState, - EnumSimulateTransferStateAction, ) from pyrevolut.api.simulations.post import ( SimulateAccountTopup, @@ -92,7 +92,7 @@ def simulate_account_topup( def simulate_transfer_state_update( self, transfer_id: UUID, - action: EnumSimulateTransferStateAction, + action: Literal["complete", "revert", "decline", "fail"], **kwargs, ) -> dict | SimulateTransferStateUpdate.Response: """ @@ -107,7 +107,7 @@ def simulate_transfer_state_update( ---------- transfer_id : UUID The ID of the transfer whose state you want to update. - action : EnumSimulateTransferStateAction + action : Literal["complete", "revert", "decline", "fail"] The action you want to perform on the transfer. Possible values: complete: @@ -125,6 +125,7 @@ def simulate_transfer_state_update( The updated transfer information. """ self.__check_sandbox() + assert action in ["complete", "revert", "decline", "fail"], "Invalid action" endpoint = SimulateTransferStateUpdate path = endpoint.ROUTE.format(transfer_id=transfer_id, action=action) body = endpoint.Body() @@ -142,10 +143,10 @@ def __check_sandbox(self): Raises ------ - InvalidEnvironmentException + PyRevolutInvalidEnvironment If the sandbox is enabled. """ if not self.client.sandbox: - raise InvalidEnvironmentException( + raise PyRevolutInvalidEnvironment( "This feature is only available in the Sandbox." ) diff --git a/pyrevolut/api/team_members/endpoint/asynchronous.py b/pyrevolut/api/team_members/endpoint/asynchronous.py index 863bf5d..31f50c9 100644 --- a/pyrevolut/api/team_members/endpoint/asynchronous.py +++ b/pyrevolut/api/team_members/endpoint/asynchronous.py @@ -1,7 +1,7 @@ from uuid import UUID from datetime import datetime -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment from pyrevolut.utils.datetime import DateTime from pyrevolut.api.common import BaseEndpointAsync from pyrevolut.api.team_members.get import RetrieveListOfTeamMembers, RetrieveTeamRoles @@ -170,10 +170,10 @@ def __check_sandbox(self): Raises ------ - InvalidEnvironmentException + PyRevolutInvalidEnvironment If the sandbox is enabled. """ if self.client.sandbox: - raise InvalidEnvironmentException( + raise PyRevolutInvalidEnvironment( "This feature is not available in Sandbox." ) diff --git a/pyrevolut/api/team_members/endpoint/synchronous.py b/pyrevolut/api/team_members/endpoint/synchronous.py index 66db017..50e75d4 100644 --- a/pyrevolut/api/team_members/endpoint/synchronous.py +++ b/pyrevolut/api/team_members/endpoint/synchronous.py @@ -1,7 +1,7 @@ from uuid import UUID from datetime import datetime -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment from pyrevolut.utils.datetime import DateTime from pyrevolut.api.common import BaseEndpointSync from pyrevolut.api.team_members.get import RetrieveListOfTeamMembers, RetrieveTeamRoles @@ -170,10 +170,10 @@ def __check_sandbox(self): Raises ------ - InvalidEnvironmentException + PyRevolutInvalidEnvironment If the sandbox is enabled. """ if self.client.sandbox: - raise InvalidEnvironmentException( + raise PyRevolutInvalidEnvironment( "This feature is not available in Sandbox." ) diff --git a/pyrevolut/api/webhooks/endpoint/base.py b/pyrevolut/api/webhooks/endpoint/base.py index 39e0080..6ec953d 100644 --- a/pyrevolut/api/webhooks/endpoint/base.py +++ b/pyrevolut/api/webhooks/endpoint/base.py @@ -1,4 +1,11 @@ +import hmac +import hashlib +import json + +import pendulum + from pyrevolut.api.common import BaseEndpointSync +from pyrevolut.exceptions import PyRevolutInvalidPayload from pyrevolut.api.webhooks.resources import ResourceWebhookPayload @@ -23,31 +30,172 @@ class BaseEndpointWebhooks(BaseEndpointSync): def receive_webhook_event( self, - payload: dict, + raw_payload: str, + signing_secret: str, + header_timestamp: int, + header_signature: str, **kwargs, ): """ - Receive a webhook event notification. + Receive a webhook event notification. Will verify the payload signature and + raise an exception if the signature is invalid. Parameters ---------- - payload : dict - The webhook event payload. + raw_payload : str + The raw payload string received from the webhook event. + signing_secret : str + The signing secret provided by Revolut for the webhook endpoint. + header_timestamp : int + The timestamp string received from the webhook event. It + will be in the header of the request under the key `Revolut-Requested-Timestamp`. + For example: 1683650202360 + header_signature : str + Signature of the request payload. It will be in the header of the request + under the key `Revolut-Signature`. + Contains the current version of the signature generating algorithm, + and the hexadecimal-encoded signature itself. + For example: v1=09a9989dd8d9282c1d34974fc730f5cbfc4f4296941247e90ae5256590a11e8c. + + It can be that there are multiple signatures in the header if multiple signing secrets are active at a given moment. + If that's the case, they are separated by a comma. + For example: + Revolut-Signature: v1=4fce70bda66b2e713be09fbb7ab1b31b0c8976ea4eeb01b244db7b99aa6482cb,v1=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39 + + Raises + ------ + PyRevolutInvalidPayload + If the payload signature is invalid or if the payload timestamp is too old. Returns ------- dict | ResourceWebhookPayload The webhook event payload. """ + # Verify the payload signature + self.verify_payload_signature( + raw_payload, signing_secret, header_timestamp, header_signature + ) + + # Parse the raw payload + raw_payload = json.loads(raw_payload) + # Raw response if self.client.return_type == "raw": - return payload + return raw_payload # Dict response - model_response = ResourceWebhookPayload(**payload) + model_response = ResourceWebhookPayload(**raw_payload) if self.client.return_type == "dict": return model_response.model_dump() # Model response if self.client.return_type == "model": return model_response + + def verify_payload_signature( + self, + raw_payload: str, + signing_secret: str, + header_timestamp: int, + header_signature: str, + ): + """Verifies the webhook payload signature. + + Parameters + ---------- + raw_payload : str + The raw payload string received from the webhook event. + signing_secret : str + The signing secret provided by Revolut for the webhook endpoint. + header_timestamp : int + The timestamp string received from the webhook event. It + will be in the header of the request under the key `Revolut-Requested-Timestamp`. + For example: 1683650202360 + header_signature : str + Signature of the request payload. It will be in the header of the request + under the key `Revolut-Signature`. + Contains the current version of the signature generating algorithm, + and the hexadecimal-encoded signature itself. + For example: v1=09a9989dd8d9282c1d34974fc730f5cbfc4f4296941247e90ae5256590a11e8c. + + It can be that there are multiple signatures in the header if multiple signing secrets are active at a given moment. + If that's the case, they are separated by a comma. + For example: + Revolut-Signature: v1=4fce70bda66b2e713be09fbb7ab1b31b0c8976ea4eeb01b244db7b99aa6482cb,v1=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39 + + Raises + ------ + PyRevolutInvalidPayload + If the payload signature is invalid or if the payload timestamp is too old. + + Returns + ------- + None + """ + # Get the revolut signature version from the signature + revolut_signature_version = header_signature.split("=")[0] + + # Check the timestamp + current_dt = pendulum.now(tz="UTC") + timestamp_dt = pendulum.from_timestamp(header_timestamp / 1000, tz="UTC") + + # Check if the timestamp is too old + if current_dt.diff(timestamp_dt).in_minutes() > 5: + raise PyRevolutInvalidPayload("The webhook payload timestamp is too old.") + + # Sign the payload + valid_signature = self.sign_payload( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=header_timestamp, + signature_version=revolut_signature_version, + ) + + # Get the multiple signatures + multiple_signatures = header_signature.split(",") + + # The computed signature must match exactly the signature (or one of the multiple signatures) + # sent in that header. + if valid_signature not in multiple_signatures: + raise PyRevolutInvalidPayload("The webhook payload signature is invalid.") + + def sign_payload( + self, + raw_payload: str, + signing_secret: str, + header_timestamp: int, + signature_version: str = "v1", + ): + """Signs the webhook payload. + + Parameters + ---------- + raw_payload : str + The raw payload string received from the webhook event. + signing_secret : str + The signing secret provided by Revolut for the webhook endpoint. + header_timestamp : int + The timestamp string received from the webhook event. It + will be in the header of the request under the key `Revolut-Requested-Timestamp`. + For example: 1683650202360 + signature_version : str + The signature version to use. Default is "v1". + + Returns + ------- + str + The signature of the request payload. + """ + # Create the payload to sign + payload_to_sign = f"{signature_version}.{header_timestamp}.{raw_payload}" + + # Sign the payload + return ( + f"{signature_version}=" + + hmac.new( + bytes(signing_secret, "utf-8"), + msg=bytes(payload_to_sign, "utf-8"), + digestmod=hashlib.sha256, + ).hexdigest() + ) diff --git a/pyrevolut/client/base.py b/pyrevolut/client/base.py index a863095..cd95d3e 100644 --- a/pyrevolut/client/base.py +++ b/pyrevolut/client/base.py @@ -5,9 +5,15 @@ from pydantic import BaseModel, Field import pendulum -from httpx import AsyncClient -from httpx import Client as SyncClient -from httpx import Request, Response +from httpx import ( + AsyncClient, + Client as SyncClient, + Request, + Response, + HTTPError, + TimeoutException, + NetworkError, +) from pyrevolut.utils.auth import ( ModelCreds, @@ -16,9 +22,19 @@ load_creds, ) from pyrevolut.exceptions import ( - PyRevolutAPIException, - BadRequestException, - InternalRevolutError, + PyRevolutBaseException, + PyRevolutTimeoutError, + PyRevolutNetworkError, + PyRevolutBadRequest, + PyRevolutUnauthorized, + PyRevolutForbidden, + PyRevolutNotFound, + PyRevolutMethodNotAllowed, + PyRevolutNotAcceptable, + PyRevolutConflict, + PyRevolutTooManyRequests, + PyRevolutInternalServerError, + PyRevolutServerUnavailable, ) @@ -162,11 +178,34 @@ def process_response( # Check for error response if response.is_error: if error_response == "raise": - if response.status_code == 400: - raise BadRequestException(response.text) - elif response.status_code // 100 == 5: - raise InternalRevolutError(response.text) - raise PyRevolutAPIException(response.text) + try: + response.raise_for_status() + except TimeoutException as exc: + raise PyRevolutTimeoutError() from exc + except NetworkError as exc: + raise PyRevolutNetworkError() from exc + except HTTPError as exc: + if response.status_code == 400: + raise PyRevolutBadRequest() from exc + elif response.status_code == 401: + raise PyRevolutUnauthorized() from exc + elif response.status_code == 403: + raise PyRevolutForbidden() from exc + elif response.status_code == 404: + raise PyRevolutNotFound() from exc + elif response.status_code == 405: + raise PyRevolutMethodNotAllowed() from exc + elif response.status_code == 406: + raise PyRevolutNotAcceptable() from exc + elif response.status_code == 409: + raise PyRevolutConflict() from exc + elif response.status_code == 429: + raise PyRevolutTooManyRequests() from exc + elif response.status_code == 500: + raise PyRevolutInternalServerError() from exc + elif response.status_code == 503: + raise PyRevolutServerUnavailable() from exc + raise PyRevolutBaseException() from exc elif error_response == "raw": return response.json() elif error_response == "dict": diff --git a/pyrevolut/exceptions/__init__.py b/pyrevolut/exceptions/__init__.py index 18d1552..721bedc 100644 --- a/pyrevolut/exceptions/__init__.py +++ b/pyrevolut/exceptions/__init__.py @@ -2,7 +2,18 @@ # flake8: noqa: F401 -from .common import PyRevolutAPIException -from .bad_request import BadRequestException -from .internal_revolut_error import InternalRevolutError -from .invalid_environment import InvalidEnvironmentException +from .common import PyRevolutBaseException +from .bad_request import PyRevolutBadRequest +from .conflict import PyRevolutConflict +from .forbidden import PyRevolutForbidden +from .internal_server_error import PyRevolutInternalServerError +from .invalid_environment import PyRevolutInvalidEnvironment +from .invalid_payload import PyRevolutInvalidPayload +from .method_not_allowed import PyRevolutMethodNotAllowed +from .network_error import PyRevolutNetworkError +from .not_acceptable import PyRevolutNotAcceptable +from .not_found import PyRevolutNotFound +from .server_unavailable import PyRevolutServerUnavailable +from .timeout_error import PyRevolutTimeoutError +from .too_many_requests import PyRevolutTooManyRequests +from .unauthorized import PyRevolutUnauthorized diff --git a/pyrevolut/exceptions/bad_request.py b/pyrevolut/exceptions/bad_request.py index 778e9fb..53e47c4 100644 --- a/pyrevolut/exceptions/bad_request.py +++ b/pyrevolut/exceptions/bad_request.py @@ -1,7 +1,13 @@ -from .common import PyRevolutAPIException +from .common import PyRevolutBaseException -class BadRequestException(PyRevolutAPIException): - """Exception raised when the API returns a 400 Bad Request error.""" +class PyRevolutBadRequest(PyRevolutBaseException): + """Bad Request -- Your request is invalid.""" - pass + def __init__( + self, + msg="Your request is invalid. Please check the request and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/common.py b/pyrevolut/exceptions/common.py index 3b9671d..a4ff279 100644 --- a/pyrevolut/exceptions/common.py +++ b/pyrevolut/exceptions/common.py @@ -1,4 +1,10 @@ -class PyRevolutAPIException(Exception): +class PyRevolutBaseException(Exception): """Base exception for all pyrevolut exceptions.""" - pass + def __init__( + self, + msg="An error occurred while processing your request to Revolut.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/conflict.py b/pyrevolut/exceptions/conflict.py new file mode 100644 index 0000000..1fce6f0 --- /dev/null +++ b/pyrevolut/exceptions/conflict.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutConflict(PyRevolutBaseException): + """Conflict -- Your request conflicts with current state of a resource.""" + + def __init__( + self, + msg="Your request conflicts with the current state of a resource. Please check the request and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/forbidden.py b/pyrevolut/exceptions/forbidden.py new file mode 100644 index 0000000..35adb45 --- /dev/null +++ b/pyrevolut/exceptions/forbidden.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutForbidden(PyRevolutBaseException): + """Forbidden -- Access to the requested resource or action is forbidden.""" + + def __init__( + self, + msg="Access to the requested resource or action is forbidden.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/internal_revolut_error.py b/pyrevolut/exceptions/internal_revolut_error.py deleted file mode 100644 index 4e4d4bd..0000000 --- a/pyrevolut/exceptions/internal_revolut_error.py +++ /dev/null @@ -1,8 +0,0 @@ -from .common import PyRevolutAPIException - - -class InternalRevolutError(PyRevolutAPIException): - """An internal error in the Revolut API. This is a bug in the Revolut API. - Please report this issue to the Revolut API team.""" - - pass diff --git a/pyrevolut/exceptions/internal_server_error.py b/pyrevolut/exceptions/internal_server_error.py new file mode 100644 index 0000000..f525423 --- /dev/null +++ b/pyrevolut/exceptions/internal_server_error.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutInternalServerError(PyRevolutBaseException): + """Internal Server Error -- We had a problem with our server. Try again later.""" + + def __init__( + self, + msg="The Revolut server had a problem. Please try again later.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/invalid_environment.py b/pyrevolut/exceptions/invalid_environment.py index 7204500..75823ae 100644 --- a/pyrevolut/exceptions/invalid_environment.py +++ b/pyrevolut/exceptions/invalid_environment.py @@ -1,7 +1,13 @@ -from .common import PyRevolutAPIException +from .common import PyRevolutBaseException -class InvalidEnvironmentException(PyRevolutAPIException): +class PyRevolutInvalidEnvironment(PyRevolutBaseException): """Raised when the environment is invalid""" - pass + def __init__( + self, + msg="This request is not valid for the current environment. Please check the environment and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/invalid_payload.py b/pyrevolut/exceptions/invalid_payload.py new file mode 100644 index 0000000..6e3de50 --- /dev/null +++ b/pyrevolut/exceptions/invalid_payload.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutInvalidPayload(PyRevolutBaseException): + """Invalid Webhook Payload -- The webhook payload is invalid.""" + + def __init__( + self, + msg="The webhook payload is invalid. Either the payload signature is invalid or the payload timestamp is too old.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/method_not_allowed.py b/pyrevolut/exceptions/method_not_allowed.py new file mode 100644 index 0000000..acfb4b1 --- /dev/null +++ b/pyrevolut/exceptions/method_not_allowed.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutMethodNotAllowed(PyRevolutBaseException): + """Method Not Allowed -- You tried to access an endpoint with an invalid method.""" + + def __init__( + self, + msg="You tried to access an endpoint with an invalid method. Please check the method and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/network_error.py b/pyrevolut/exceptions/network_error.py new file mode 100644 index 0000000..14527cc --- /dev/null +++ b/pyrevolut/exceptions/network_error.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutNetworkError(PyRevolutBaseException): + """Network Error -- The request failed due to a network error.""" + + def __init__( + self, + msg="The request failed due to a network error. Please check your network connection and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/not_acceptable.py b/pyrevolut/exceptions/not_acceptable.py new file mode 100644 index 0000000..2d5ca15 --- /dev/null +++ b/pyrevolut/exceptions/not_acceptable.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutNotAcceptable(PyRevolutBaseException): + """Not Acceptable -- You requested a format that isn't JSON.""" + + def __init__( + self, + msg="You requested a format that isn't JSON. Please check the request and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/not_found.py b/pyrevolut/exceptions/not_found.py new file mode 100644 index 0000000..1dd16f5 --- /dev/null +++ b/pyrevolut/exceptions/not_found.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutNotFound(PyRevolutBaseException): + """Not Found -- The requested resource could not be found.""" + + def __init__( + self, + msg="The requested resource could not be found on the Revolut server.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/server_unavailable.py b/pyrevolut/exceptions/server_unavailable.py new file mode 100644 index 0000000..652dc3a --- /dev/null +++ b/pyrevolut/exceptions/server_unavailable.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutServerUnavailable(PyRevolutBaseException): + """Service Unavailable -- We're temporarily offline for maintenance. Please try again later.""" + + def __init__( + self, + msg="The Revolut server is temporarily offline for maintenance. Please try again later.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/timeout_error.py b/pyrevolut/exceptions/timeout_error.py new file mode 100644 index 0000000..34a2291 --- /dev/null +++ b/pyrevolut/exceptions/timeout_error.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutTimeoutError(PyRevolutBaseException): + """Timeout Error -- The request timed out.""" + + def __init__( + self, + msg="The request timed out. Please try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/too_many_requests.py b/pyrevolut/exceptions/too_many_requests.py new file mode 100644 index 0000000..911aac5 --- /dev/null +++ b/pyrevolut/exceptions/too_many_requests.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutTooManyRequests(PyRevolutBaseException): + """Too Many Requests -- You're sending too many requests.""" + + def __init__( + self, + msg="You're sending too many requests. Please wait a while and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/pyrevolut/exceptions/unauthorized.py b/pyrevolut/exceptions/unauthorized.py new file mode 100644 index 0000000..32b490a --- /dev/null +++ b/pyrevolut/exceptions/unauthorized.py @@ -0,0 +1,13 @@ +from .common import PyRevolutBaseException + + +class PyRevolutUnauthorized(PyRevolutBaseException): + """Unauthorized -- Your access token is wrong.""" + + def __init__( + self, + msg="Your access token is wrong. Please check the access token and try again.", + *args, + **kwargs, + ): + super().__init__(msg, *args, **kwargs) diff --git a/tests/app/__init__.py b/tests/app/__init__.py new file mode 100644 index 0000000..fdc4394 --- /dev/null +++ b/tests/app/__init__.py @@ -0,0 +1,62 @@ +# flake8: noqa: F401 +import threading +import contextlib +import time + +from litestar import Litestar, status_codes +from litestar.config.cors import CORSConfig + +import uvicorn + +from .app import ( + index, + webhook, + test_raw_payload, + set_signing_secret, + get_signing_secret, + get_webhook_payload, + internal_server_error_handler, +) + +app = Litestar( + route_handlers=[ + index, + webhook, + set_signing_secret, + test_raw_payload, + get_signing_secret, + get_webhook_payload, + ], + cors_config=CORSConfig(allow_origins=["*"]), + exception_handlers={ + status_codes.HTTP_500_INTERNAL_SERVER_ERROR: internal_server_error_handler, + }, +) + + +class UvicornServer(uvicorn.Server): + def install_singal_handlers(self): + pass + + @contextlib.contextmanager + def run_in_thread(self): + """Run the server in a separate thread. + + Parameters + ---------- + None + + Yields + ------ + None + + """ + thread = threading.Thread(target=self.run) + thread.start() + try: + while not self.started: + time.sleep(1e-3) + yield + finally: + self.should_exit = True + thread.join() diff --git a/tests/app/app.py b/tests/app/app.py new file mode 100644 index 0000000..b123dc9 --- /dev/null +++ b/tests/app/app.py @@ -0,0 +1,106 @@ +from typing import Annotated, Any +import json +import logging + +from litestar import Request, Response, get, post, status_codes +from litestar.params import Body, Parameter +from litestar.datastructures import State +from litestar.exceptions import ValidationException + +from pyrevolut.client import AsyncClient +from pyrevolut.api.webhooks.resources import ResourceWebhookPayload + + +def internal_server_error_handler(request: Request, exc: Exception) -> Response: + """This function will handle all internal server errors + + Parameters + ---------- + request: Request + The request object + exc: Exception + The exception that was raised + + Returns + ------- + Response + The response object + """ + logging.error( + { + "path": request.url.path, + "method": request.method, + "query_params": request.query_params, + "reason": str(exc), + } + ) + return Response( + status_code=status_codes.HTTP_500_INTERNAL_SERVER_ERROR, + content={"detail": "Internal Server Error"}, + ) + + +@get("/") +async def index() -> dict[str, str]: + return {"message": "Hello, World!"} + + +@post("/test-raw-payload") +async def test_raw_payload( + request: Request, + data: Annotated[dict[str, Any], Body(description="The raw payload")], +) -> dict[str, str]: + raw_payload = json.dumps(await request.json()) + print("Raw payload", raw_payload) + return {"message": f"Raw payload: {raw_payload}"} + + +@post("/signing-secret") +async def set_signing_secret( + state: State, + data: Annotated[dict[str, str], Body(description="The signing secret")], +) -> dict[str, str]: + if "signing_secret" not in data: + raise ValidationException( + "Signing secret not provided! Make sure to include the 'signing_secret' key in the request body." + ) + state.signing_secret = data["signing_secret"] + return {"message": "Signing secret set!"} + + +@get("/signing-secret") +async def get_signing_secret(state: State) -> dict[str, str]: + return {"signing_secret": state.signing_secret} + + +@post("/webhook") +async def webhook( + state: State, + request: Request, + signature: Annotated[str, Parameter(header="Revolut-Signature")], + timestamp: Annotated[int, Parameter(header="Revolut-Request-Timestamp")], +) -> dict[str, str]: + client: AsyncClient = state.client + signing_secret: str = state.signing_secret + raw_payload = (await request.body()).decode("utf-8") + + logging.info("Webhook received with payload: %s", raw_payload) + + # Validate the webhook payload + result: ResourceWebhookPayload = client.Webhooks.receive_webhook_event( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=timestamp, + header_signature=signature, + ) + + # Store the webhook payload + state.webhook_payload = result + return {"message": "Webhook received successfully!"} + + +@get("/webhook") +async def get_webhook_payload(state: State) -> ResourceWebhookPayload: + if not state.webhook_payload: + raise ValidationException("No webhook payload received yet!") + return state.webhook_payload diff --git a/tests/conftest.py b/tests/conftest.py index db1e3d6..1ebfd77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,10 +7,29 @@ import pytest import pytest_asyncio +from dotenv import load_dotenv +import uvicorn + +from litestar import Litestar, status_codes +from litestar.config.cors import CORSConfig +from litestar.datastructures import State +from pyngrok import ngrok +from pyngrok.conf import PyngrokConfig from pyrevolut.client import Client, AsyncClient from pyrevolut.api import EnumTransactionState +from tests.app import ( + UvicornServer, + index, + webhook, + test_raw_payload, + set_signing_secret, + get_signing_secret, + get_webhook_payload, + internal_server_error_handler, +) + """ Pytest Fixture Scopes 1. function: the default scope, the fixture is destroyed at the end of the test. @@ -163,3 +182,70 @@ async def async_client(base_async_client: AsyncClient): # Close the async client await base_async_client.close() + + +@pytest_asyncio.fixture(scope="function") +async def litestar_client_url(async_client: AsyncClient): + """Context manager that initializes the Litestar client + + Parameters + ---------- + async_client : AsyncClient + The async client to use for the endpoint + + Yields + ------ + str + The url of the Litestar client + """ + # Create the Litestar app + app = Litestar( + route_handlers=[ + index, + webhook, + set_signing_secret, + test_raw_payload, + get_signing_secret, + get_webhook_payload, + ], + cors_config=CORSConfig(allow_origins=["*"]), + state=State({"client": async_client}), + exception_handlers={ + status_codes.HTTP_500_INTERNAL_SERVER_ERROR: internal_server_error_handler, + }, + ) + + # Get the Environment variables + dotenv_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "tests/.env", + ) + + # Load .env file variables + load_dotenv(dotenv_path=dotenv_path) + + # Initialize the ngrok tunnel + ngrok_config = PyngrokConfig(auth_token=os.getenv("NGROK_AUTH_TOKEN")) + ngrok_tunnel = ngrok.connect( + addr="8000", + pyngrok_config=ngrok_config, + ) + public_url = ngrok_tunnel.public_url + + # Start the Litestar app in a seaparate thread + uvicorn_config = uvicorn.Config( + app=app, + host="0.0.0.0", + port=8000, + log_level="error", + ) + with UvicornServer(config=uvicorn_config).run_in_thread(): + # Wait a few seconds for startup + await asyncio.sleep(3) + + # Yield public URL for test + yield public_url + + # Shutdown the ngrok tunnel + ngrok.disconnect(public_url=public_url) + ngrok.kill() diff --git a/tests/credentials/README.md b/tests/credentials/README.md index ad2437b..056a7e7 100644 --- a/tests/credentials/README.md +++ b/tests/credentials/README.md @@ -1,5 +1,17 @@ # Credentials -For testing purposes, place your credentials in this directory. They will be ignored by git. +## Revolut + +For testing purposes, place your Revolut credentials in this directory. They will be ignored by git. It is possible to place multiple credentials files in this directory, as long as they have the same format. The tests will randomly pick one of the files per test. + +## Environment Variables + +The testing environment will look for an .env file in this directory. The file should contain the following variables: + +```bash +NGROK_AUTH_TOKEN=2hK5kZLB9b5w9IhW6eXp8uP4Ri1_7sMkjEw8WDLUQoL93DbgY +``` + +The NGROK_AUTH_TOKEN is used to authenticate the ngrok tunnel so the tests can run against the local server. diff --git a/tests/test_card.py b/tests/test_card.py index c4a6b93..40fed89 100644 --- a/tests/test_card.py +++ b/tests/test_card.py @@ -4,14 +4,14 @@ import random from pyrevolut.client import Client, AsyncClient -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment def test_sync_get_all_cards(sync_client: Client): """Test the sync `get_all_cards` cards method""" # Get Cards (no params) with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): cards_all = sync_client.Cards.get_all_cards() time.sleep(random.randint(1, 3)) @@ -21,7 +21,7 @@ def test_sync_get_all_cards(sync_client: Client): # Get Cards (with params) with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): cards_all = sync_client.Cards.get_all_cards( created_before="2020-01-01", @@ -35,7 +35,7 @@ def test_sync_get_all_cards(sync_client: Client): def test_sync_get_card(sync_client: Client): """Test the sync `get_card` cards method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all cards cards_all = sync_client.Cards.get_all_cards() @@ -54,7 +54,7 @@ def test_sync_get_card(sync_client: Client): def test_get_card_sensitive_details(sync_client: Client): """Test the sync `get_card_sensitive_details` cards method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all cards cards_all = sync_client.Cards.get_all_cards() @@ -100,7 +100,7 @@ async def test_async_get_all_cards(async_client: AsyncClient): """Test the async `get_all_cards` cards method""" # Get Cards (no params) with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): cards_all = await async_client.Cards.get_all_cards() await asyncio.sleep(random.randint(1, 3)) @@ -110,7 +110,7 @@ async def test_async_get_all_cards(async_client: AsyncClient): # Get Cards (with params) with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): cards_all = await async_client.Cards.get_all_cards( created_before="2020-01-01", @@ -125,7 +125,7 @@ async def test_async_get_all_cards(async_client: AsyncClient): async def test_async_get_card(async_client: AsyncClient): """Test the async `get_card` cards method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all cards cards_all = await async_client.Cards.get_all_cards() @@ -145,7 +145,7 @@ async def test_async_get_card(async_client: AsyncClient): async def test_async_get_card_sensitive_details(async_client: AsyncClient): """Test the async `get_card_sensitive_details` cards method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all cards cards_all = await async_client.Cards.get_all_cards() diff --git a/tests/test_foreign_exchange.py b/tests/test_foreign_exchange.py index e5614e7..a08a9f4 100644 --- a/tests/test_foreign_exchange.py +++ b/tests/test_foreign_exchange.py @@ -6,7 +6,7 @@ from pyrevolut.client import Client, AsyncClient from pyrevolut.api import EnumAccountState, EnumTransactionState -from pyrevolut.exceptions import InternalRevolutError +from pyrevolut.exceptions import PyRevolutInternalServerError def test_sync_get_exchange_rate(sync_client: Client): @@ -125,7 +125,7 @@ def test_sync_exchange_money(sync_client: Client): assert gbp_balance3 < gbp_balance2 assert eur_balance3 == eur_balance assert eur_balance3 > eur_balance2 - except InternalRevolutError: + except PyRevolutInternalServerError: # This error occurs randomly in the sandbox environment pass @@ -248,6 +248,6 @@ async def test_async_exchange_money(async_client: AsyncClient): assert gbp_balance3 < gbp_balance2 assert eur_balance3 == eur_balance assert eur_balance3 > eur_balance2 - except InternalRevolutError: + except PyRevolutInternalServerError: # This error occurs randomly in the sandbox environment pass diff --git a/tests/test_payment_drafts.py b/tests/test_payment_drafts.py index 3637b92..47ac6fd 100644 --- a/tests/test_payment_drafts.py +++ b/tests/test_payment_drafts.py @@ -5,7 +5,7 @@ from pyrevolut.client import Client from pyrevolut.api import EnumAccountState -from pyrevolut.exceptions import InternalRevolutError +from pyrevolut.exceptions import PyRevolutInternalServerError def test_sync_get_all_payment_drafts(sync_client: Client): @@ -79,7 +79,7 @@ def test_sync_create_delete_payment_draft(sync_client: Client): # Delete the payment draft sync_client.PaymentDrafts.delete_payment_draft(payment_draft_id=response["id"]) time.sleep(random.randint(1, 3)) - except InternalRevolutError: + except PyRevolutInternalServerError: # This error occurs randomly in the sandbox environment pass @@ -160,6 +160,6 @@ async def test_async_create_delete_payment_draft(async_client: Client): payment_draft_id=response["id"] ) await asyncio.sleep(random.randint(1, 3)) - except InternalRevolutError: + except PyRevolutInternalServerError: # This error occurs randomly in the sandbox environment pass diff --git a/tests/test_simulations.py b/tests/test_simulations.py index 68eee9b..ff3b11e 100644 --- a/tests/test_simulations.py +++ b/tests/test_simulations.py @@ -9,7 +9,6 @@ EnumAccountState, EnumTransactionState, EnumTransferReasonCode, - EnumSimulateTransferStateAction, ) @@ -136,7 +135,7 @@ def test_sync_simulate_transfer_state_update(sync_client: Client): # Decline the transfer response = sync_client.Simulations.simulate_transfer_state_update( transfer_id=response["id"], - action=EnumSimulateTransferStateAction.DECLINE, + action="decline", ) time.sleep(random.randint(1, 3)) assert response["state"] == EnumTransactionState.DECLINED @@ -271,7 +270,7 @@ async def test_async_simulate_transfer_state_update(async_client: Client): # Decline the transfer response = await async_client.Simulations.simulate_transfer_state_update( transfer_id=response["id"], - action=EnumSimulateTransferStateAction.DECLINE, + action="decline", ) await asyncio.sleep(random.randint(1, 3)) assert response["state"] == EnumTransactionState.DECLINED diff --git a/tests/test_team_members.py b/tests/test_team_members.py index d239a39..246d655 100644 --- a/tests/test_team_members.py +++ b/tests/test_team_members.py @@ -4,14 +4,14 @@ import random from pyrevolut.client import Client -from pyrevolut.exceptions import InvalidEnvironmentException +from pyrevolut.exceptions import PyRevolutInvalidEnvironment def test_sync_get_team_members(sync_client: Client): """Test the sync `get_team_members` team members method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all team members (no params) team_members = sync_client.TeamMembers.get_team_members() @@ -34,7 +34,7 @@ def test_sync_get_team_roles(sync_client: Client): """Test the sync `get_team_roles` team members method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all team roles (no params) team_roles = sync_client.TeamMembers.get_team_roles() @@ -57,7 +57,7 @@ def test_sync_invite_team_member(sync_client: Client): """Test the sync `invite_team_member` team members method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all team roles team_roles = sync_client.TeamMembers.get_team_roles() @@ -81,7 +81,7 @@ async def test_async_get_team_members(async_client: Client): """Test the async `get_team_members` team members method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all team members (no params) team_members = await async_client.TeamMembers.get_team_members() @@ -105,7 +105,7 @@ async def test_async_get_team_roles(async_client: Client): """Test the async `get_team_roles` team members method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all team roles (no params) team_roles = await async_client.TeamMembers.get_team_roles() @@ -129,7 +129,7 @@ async def test_async_invite_team_member(async_client: Client): """Test the async `invite_team_member` team members method""" with pytest.raises( - InvalidEnvironmentException, match="This feature is not available in Sandbox." + PyRevolutInvalidEnvironment, match="This feature is not available in Sandbox." ): # Get all team roles team_roles = await async_client.TeamMembers.get_team_roles() diff --git a/tests/test_transfers.py b/tests/test_transfers.py index cdf5916..967925e 100644 --- a/tests/test_transfers.py +++ b/tests/test_transfers.py @@ -10,7 +10,6 @@ EnumAccountState, EnumTransactionState, EnumTransferReasonCode, - EnumSimulateTransferStateAction, ) @@ -198,7 +197,7 @@ def test_sync_create_transfer_to_another_account(sync_client: Client): # Complete the transfer via simulation response = sync_client.Simulations.simulate_transfer_state_update( transfer_id=response["id"], - action=EnumSimulateTransferStateAction.COMPLETE, + action="complete", ) time.sleep(random.randint(1, 3)) assert response["state"] == EnumTransactionState.COMPLETED @@ -225,7 +224,7 @@ def test_sync_create_transfer_to_another_account(sync_client: Client): # Complete the transfer via simulation response = sync_client.Simulations.simulate_transfer_state_update( transfer_id=response["id"], - action=EnumSimulateTransferStateAction.COMPLETE, + action="complete", ) time.sleep(random.randint(1, 3)) assert response["state"] == EnumTransactionState.COMPLETED @@ -418,7 +417,7 @@ async def test_async_create_transfer_to_another_account(async_client: Client): # Complete the transfer via simulation response = await async_client.Simulations.simulate_transfer_state_update( transfer_id=response["id"], - action=EnumSimulateTransferStateAction.COMPLETE, + action="complete", ) await asyncio.sleep(random.randint(1, 3)) assert response["state"] == EnumTransactionState.COMPLETED @@ -445,7 +444,7 @@ async def test_async_create_transfer_to_another_account(async_client: Client): # Complete the transfer via simulation response = await async_client.Simulations.simulate_transfer_state_update( transfer_id=response["id"], - action=EnumSimulateTransferStateAction.COMPLETE, + action="complete", ) await asyncio.sleep(random.randint(1, 3)) assert response["state"] == EnumTransactionState.COMPLETED diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 7ef8b84..bbab0c0 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -1,11 +1,31 @@ import time import asyncio import random +from uuid import UUID, uuid4 +import platform import pytest +import pendulum +from httpx import AsyncClient as TestHTTPClient from pyrevolut.client import Client -from pyrevolut.api import EnumWebhookEvent +from pyrevolut.api import ( + EnumWebhookEvent, + EnumAccountState, + EnumTransactionState, + EnumTransferReasonCode, +) +from pyrevolut.api.webhooks.post import CreateWebhook +from pyrevolut.api.webhooks.resources import ( + ResourceWebhookPayload, + ResourceTransactionCreated, + ResourceTransactionStateChanged, +) +from pyrevolut.api.accounts.get import RetrieveAllAccounts +from pyrevolut.api.simulations.post import SimulateAccountTopup +from pyrevolut.api.counterparties.get import RetrieveListOfCounterparties +from pyrevolut.api.transfers.post import CreateTransferToAnotherAccount +from pyrevolut.exceptions import PyRevolutInvalidPayload def test_sync_get_all_webhooks(sync_client: Client): @@ -204,3 +224,224 @@ async def test_async_create_update_rotate_delete_webhook(async_client: Client): webhooks = await async_client.Webhooks.get_all_webhooks() await asyncio.sleep(random.randint(1, 3)) assert webhook["id"] not in [webhook["id"] for webhook in webhooks] + + +def test_webhook_sign_payload(sync_client: Client): + """Test that the webhook payload signing works""" + # Params + revolut_signature_header = ( + "v1=bca326fb378d0da7f7c490ad584a8106bab9723d8d9cdd0d50b4c5b3be3837c0" + ) + revolut_request_timestamp = 1683650202360 + signing_secret = "wsk_r59a4HfWVAKycbCaNO1RvgCJec02gRd8" + raw_payload = '{"data":{"id":"645a7696-22f3-aa47-9c74-cbae0449cc46","new_state":"completed","old_state":"pending","request_id":"app_charges-9f5d5eb3-1e06-46c5-b1c0-3914763e0bcb"},"event":"TransactionStateChanged","timestamp":"2023-05-09T16:36:38.028960Z"}' + + # Sign the payload + payload_signed = sync_client.Webhooks.sign_payload( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=revolut_request_timestamp, + signature_version="v1", + ) + + # Check the signature + assert payload_signed == revolut_signature_header + + +def test_webhook_verify_payload_signature_valid(sync_client: Client): + raw_payload = '{"event": "TransactionCreated"}' + signing_secret = "my_signing_secret" + header_timestamp = pendulum.now(tz="UTC").int_timestamp * 1000 + header_signature = sync_client.Webhooks.sign_payload( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=header_timestamp, + signature_version="v1", + ) + + # No exception should be raised + sync_client.Webhooks.verify_payload_signature( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=header_timestamp, + header_signature=header_signature, + ) + + +def test_webhook_verify_payload_signature_invalid_signature(sync_client: Client): + raw_payload = '{"event": "TransactionCreated"}' + signing_secret = "my_signing_secret" + header_timestamp = pendulum.now(tz="UTC").int_timestamp * 1000 + header_signature = "v1=invalid_signature" + + # PyRevolutInvalidPayload exception should be raised + with pytest.raises( + PyRevolutInvalidPayload, match="The webhook payload signature is invalid." + ): + sync_client.Webhooks.verify_payload_signature( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=header_timestamp, + header_signature=header_signature, + ) + + +def test_webhook_verify_payload_signature_invalid_timestamp(sync_client: Client): + raw_payload = '{"event": "TransactionCreated"}' + signing_secret = "my_signing_secret" + header_timestamp = 1683650200000 # Older timestamp + header_signature = sync_client.Webhooks.sign_payload( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=header_timestamp, + signature_version="v1", + ) + + # PyRevolutInvalidPayload exception should be raised + with pytest.raises( + PyRevolutInvalidPayload, match="The webhook payload timestamp is too old." + ): + sync_client.Webhooks.verify_payload_signature( + raw_payload=raw_payload, + signing_secret=signing_secret, + header_timestamp=header_timestamp, + header_signature=header_signature, + ) + + +@pytest.mark.asyncio +@pytest.mark.skipif( + condition=platform.system() != "Darwin", + reason="Only one ngrok tunnel allowed at a time", +) +async def test_webhook_app(async_client: Client, litestar_client_url: str): + """Test the webhook when receiving a webhook""" + # Set the async client to return pydantic models + async_client.return_type = "model" + + # Create a new webhook + webhook: CreateWebhook.Response = await async_client.Webhooks.create_webhook( + url=litestar_client_url + "/webhook", + events=[ + EnumWebhookEvent.TRANSACTION_CREATED, + EnumWebhookEvent.TRANSACTION_STATE_CHANGED, + EnumWebhookEvent.PAYOUT_LINK_CREATED, + EnumWebhookEvent.PAYOUT_LINK_STATE_CHANGED, + ], + ) + await asyncio.sleep(random.randint(1, 3)) + + async with TestHTTPClient() as http_client: + # Set the signing secret + resp = await http_client.post( + url=f"{litestar_client_url}/signing-secret", + json={"signing_secret": webhook.signing_secret}, + ) + assert resp.status_code == 201 + assert resp.json() == {"message": "Signing secret set!"} + + # Get the signing secret + resp = await http_client.get(url=f"{litestar_client_url}/signing-secret") + assert resp.status_code == 200 + assert resp.json() == {"signing_secret": webhook.signing_secret} + + ### Create a transaction ### + + # Get all accounts + accounts: list[ + RetrieveAllAccounts.Response + ] = await async_client.Accounts.get_all_accounts() + await asyncio.sleep(random.randint(1, 3)) + + # Get EUR account + eur_account = next( + account + for account in accounts + if account.currency == "EUR" + and account.state == EnumAccountState.ACTIVE + and account.balance > 0.0 + ) + eur_balance = eur_account.balance + + # If there is no EUR balance, simulate a top up + if eur_balance < 1.0: + response: SimulateAccountTopup.Response = ( + await async_client.Simulations.simulate_account_topup( + account_id=eur_account.id, + amount=1.0, + currency="EUR", + reference="PyRevolut Test", + state=EnumTransactionState.COMPLETED, + ) + ) + await asyncio.sleep(random.randint(1, 3)) + assert response.state == EnumTransactionState.COMPLETED + + # Get all counterparties + counterparties: list[ + RetrieveListOfCounterparties.Response + ] = await async_client.Counterparties.get_all_counterparties() + + # Get a EUR counterparty with an IBAN + eur_counterparties: list[RetrieveListOfCounterparties.Response] = [] + for counterparty in counterparties: + counterparty_accounts = counterparty.accounts or [] + for account in counterparty_accounts: + if account.currency == "EUR" and account.iban is not None: + eur_counterparties.append(counterparty) + + # Get the first EUR counterparty + eur_counterparty = eur_counterparties[0] + eur_counterparty_account = [ + acc + for acc in eur_counterparty.accounts or [] + if acc.currency == "EUR" and acc.iban is not None + ][0] + + # Create a transfer to the EUR counterparty + response: CreateTransferToAnotherAccount.Response = ( + await async_client.Transfers.create_transfer_to_another_account( + request_id=str(uuid4()), + account_id=eur_account.id, + counterparty_id=eur_counterparty.id, + amount=1.0, + currency="EUR", + counterparty_account_id=eur_counterparty_account.id, + reference="PyRevolut Test", + transfer_reason_code=EnumTransferReasonCode.FAMILY_SUPPORT, + ) + ) + await asyncio.sleep(3) + assert response.state == EnumTransactionState.PENDING + + # Check that the webhook was received + async with TestHTTPClient() as http_client: + resp = await http_client.get(url=f"{litestar_client_url}/webhook") + assert resp.status_code == 200 + response_data = ResourceWebhookPayload(**resp.json()) + assert response_data.event == EnumWebhookEvent.TRANSACTION_CREATED + assert isinstance(response_data.data, ResourceTransactionCreated) + assert response_data.data.id == UUID(response.id) + assert response_data.data.state == EnumTransactionState.PENDING + + # Simulate a transfer state change + await async_client.Simulations.simulate_transfer_state_update( + transfer_id=response.id, + action="complete", + ) + await asyncio.sleep(3) + + # Check that the webhook was received + async with TestHTTPClient() as http_client: + resp = await http_client.get(url=f"{litestar_client_url}/webhook") + assert resp.status_code == 200 + response_data = ResourceWebhookPayload(**resp.json()) + assert response_data.event == EnumWebhookEvent.TRANSACTION_STATE_CHANGED + assert isinstance(response_data.data, ResourceTransactionStateChanged) + assert response_data.data.id == UUID(response.id) + assert response_data.data.new_state == EnumTransactionState.COMPLETED + assert response_data.data.old_state == EnumTransactionState.PENDING + + # Delete the webhook + await async_client.Webhooks.delete_webhook(webhook_id=webhook.id) + await asyncio.sleep(random.randint(1, 3))