diff --git a/freedium-library/pdm.lock b/freedium-library/pdm.lock index 597efe2..fde4604 100644 --- a/freedium-library/pdm.lock +++ b/freedium-library/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "api", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:c8d5376c4072286f4b254f030eb7bf2e315723b69831378eea9d454568df6bad" +content_hash = "sha256:8e8f44f9745b84c33f16714dc879a9ba33fbb782fe6d6a4455092936bf3d6733" [[metadata.targets]] requires_python = "==3.12.*" @@ -15,7 +15,7 @@ name = "annotated-types" version = "0.7.0" requires_python = ">=3.8" summary = "Reusable constraint types to use with typing.Annotated" -groups = ["api"] +groups = ["default", "api"] dependencies = [ "typing-extensions>=4.0.0; python_version < \"3.9\"", ] @@ -42,19 +42,31 @@ files = [ ] [[package]] -name = "asyncclick" -version = "8.1.7.2" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit, async version" +name = "beautifulsoup4" +version = "4.13.4" +requires_python = ">=3.7.0" +summary = "Screen-scraping library" groups = ["default"] dependencies = [ - "anyio", - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", + "soupsieve>1.2", + "typing-extensions>=4.0.0", ] files = [ - {file = "asyncclick-8.1.7.2-py3-none-any.whl", hash = "sha256:1ab940b04b22cb89b5b400725132b069d01b0c3472a9702c7a2c9d5d007ded02"}, - {file = "asyncclick-8.1.7.2.tar.gz", hash = "sha256:219ea0f29ccdc1bb4ff43bcab7ce0769ac6d48a04f997b43ec6bee99a222daa0"}, + {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, + {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, +] + +[[package]] +name = "bs4" +version = "0.0.2" +summary = "Dummy package for Beautiful Soup (beautifulsoup4)" +groups = ["default"] +dependencies = [ + "beautifulsoup4", +] +files = [ + {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, + {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, ] [[package]] @@ -68,6 +80,30 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -84,7 +120,7 @@ name = "click" version = "8.1.7" requires_python = ">=3.7" summary = "Composable command line interface toolkit" -groups = ["default", "api"] +groups = ["api"] dependencies = [ "colorama; platform_system == \"Windows\"", "importlib-metadata; python_version < \"3.8\"", @@ -100,12 +136,102 @@ version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." groups = ["default", "api", "dev"] -marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.8.2" +requires_python = ">=3.9" +summary = "Code coverage measurement for Python" +groups = ["dev"] +files = [ + {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, + {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, + {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, + {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, + {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, + {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, +] + +[[package]] +name = "coverage" +version = "7.8.2" +extras = ["toml"] +requires_python = ">=3.9" +summary = "Code coverage measurement for Python" +groups = ["dev"] +dependencies = [ + "coverage==7.8.2", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, + {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, + {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, + {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, + {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, + {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, + {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, + {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, + {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, +] + +[[package]] +name = "curl-cffi" +version = "0.11.2" +requires_python = ">=3.9" +summary = "libcurl ffi bindings for Python, with impersonation support." +groups = ["default"] +dependencies = [ + "certifi>=2024.2.2", + "cffi>=1.12.0", +] +files = [ + {file = "curl_cffi-0.11.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:eddd85de355ca11dade9348cd4d6cbb5f840da38bc309aa7cd4b2a73fe78bde2"}, + {file = "curl_cffi-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1c5231974e236f89a63efa80859b2f6bc46ad606244c9e18df135926cd51b223"}, + {file = "curl_cffi-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ae26a58e85b1386095592b1e455b79befc8349bccb29a354f8fe6ed45caf2bd"}, + {file = "curl_cffi-0.11.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8830d10bfafc29e345b66be352fd3ef3253872aad131ad99e2959e0063c155d"}, + {file = "curl_cffi-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6485b641525d2b292a1be7ea7c3d68139ddc8e2654d54c7723337ca717df2f9"}, + {file = "curl_cffi-0.11.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:61e1e0d628d7af1d4cffb99a017bfbdf1ff0dc85c0b7ae7e2f7f6ed756967766"}, + {file = "curl_cffi-0.11.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1b59a88ba8184594fa3a2dca5907cb0a35acaa3604e0961346dbd90f57e91f49"}, + {file = "curl_cffi-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:850a6024ef0e7907ac2de23c070a8f296be116796bec51a753c6e8e9059cf8b7"}, + {file = "curl_cffi-0.11.2.tar.gz", hash = "sha256:31b623a45ce47a917f25d9099ba8b3b7259b2a9ea151c3237c12b03e22e5c6ae"}, +] + +[[package]] +name = "dependency-injector" +version = "4.47.1" +requires_python = ">=3.8" +summary = "Dependency injection framework for Python" +groups = ["default"] +files = [ + {file = "dependency_injector-4.47.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:69fc6fb277f3eb8635aa807a79fa0c7b0a57ba47179d5b24312db083aef3ce0f"}, + {file = "dependency_injector-4.47.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6798041de0f21194c721618c5d9bab254c9b7bc1fc7b2e80375482dd084b8000"}, + {file = "dependency_injector-4.47.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3207090ee2d8d01588e0c1c967e19684653d7c36fa55558e8e4b8144cc1bc382"}, + {file = "dependency_injector-4.47.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5656c7b3369caf37e13ac4714b96013de286066cec0bf9d72d5c59ba252485f7"}, + {file = "dependency_injector-4.47.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c3dad8274ad0f16d45743d5fb7cdcae2ecdfd5e9f6d5cf5a59211b6e7601691f"}, + {file = "dependency_injector-4.47.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:ae2f3bfbfbda19d73afa98001b91b2b50b3ce076adece1b23daa8633890b6345"}, + {file = "dependency_injector-4.47.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:07d7cc56df5994a0004dce0f54106a9ed0b0b779161ab58fe2d41014e0c9bc36"}, + {file = "dependency_injector-4.47.1-cp38-abi3-win32.whl", hash = "sha256:5a7a24f6ab6fee574002e36e7c7daa4a86041d2c02a95259beac38d0b46fcd7f"}, + {file = "dependency_injector-4.47.1-cp38-abi3-win_amd64.whl", hash = "sha256:c04a897b761e2c55bb71986fdac721db6583d474699bc1d99c51764f11b592b6"}, + {file = "dependency_injector-4.47.1.tar.gz", hash = "sha256:b2681b054af767874bf57a17593257d303eec54950515887605e586bbe5c3f05"}, +] + [[package]] name = "deprecated" version = "1.2.14" @@ -180,35 +306,20 @@ files = [ {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, ] -[[package]] -name = "faker" -version = "30.8.1" -requires_python = ">=3.8" -summary = "Faker is a Python package that generates fake data for you." -groups = ["default"] -dependencies = [ - "python-dateutil>=2.4", - "typing-extensions", -] -files = [ - {file = "Faker-30.8.1-py3-none-any.whl", hash = "sha256:4f7f133560b9d4d2a915581f4ba86f9a6a83421b89e911f36c4c96cff58135a5"}, - {file = "faker-30.8.1.tar.gz", hash = "sha256:93e8b70813f76d05d98951154681180cb795cfbcff3eced7680d963bcc0da2a9"}, -] - [[package]] name = "fastapi" -version = "0.115.5" +version = "0.115.12" requires_python = ">=3.8" summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" groups = ["api"] dependencies = [ "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", - "starlette<0.42.0,>=0.40.0", + "starlette<0.47.0,>=0.40.0", "typing-extensions>=4.8.0", ] files = [ - {file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"}, - {file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"}, + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, ] [[package]] @@ -244,7 +355,7 @@ files = [ [[package]] name = "fastapi" -version = "0.115.5" +version = "0.115.12" extras = ["standard"] requires_python = ">=3.8" summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" @@ -252,15 +363,15 @@ groups = ["api"] dependencies = [ "email-validator>=2.0.0", "fastapi-cli[standard]>=0.0.5", - "fastapi==0.115.5", + "fastapi==0.115.12", "httpx>=0.23.0", - "jinja2>=2.11.2", - "python-multipart>=0.0.7", + "jinja2>=3.1.5", + "python-multipart>=0.0.18", "uvicorn[standard]>=0.12.0", ] files = [ - {file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"}, - {file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"}, + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, ] [[package]] @@ -274,6 +385,20 @@ files = [ {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] +[[package]] +name = "freezegun" +version = "1.5.2" +requires_python = ">=3.8" +summary = "Let your Python tests travel through time" +groups = ["dev"] +dependencies = [ + "python-dateutil>=2.7", +] +files = [ + {file = "freezegun-1.5.2-py3-none-any.whl", hash = "sha256:5aaf3ba229cda57afab5bd311f0108d86b6fb119ae89d2cd9c43ec8c1733c85b"}, + {file = "freezegun-1.5.2.tar.gz", hash = "sha256:a54ae1d2f9c02dbf42e02c18a3ab95ab4295818b549a34dac55592d72a905181"}, +] + [[package]] name = "h11" version = "0.14.0" @@ -322,7 +447,7 @@ files = [ [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" requires_python = ">=3.8" summary = "The next generation HTTP client." groups = ["default", "api", "dev"] @@ -331,27 +456,26 @@ dependencies = [ "certifi", "httpcore==1.*", "idna", - "sniffio", ] files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" extras = ["socks"] requires_python = ">=3.8" summary = "The next generation HTTP client." groups = ["default"] dependencies = [ - "httpx==0.27.2", + "httpx==0.28.1", "socksio==1.*", ] files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [[package]] @@ -403,7 +527,7 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" requires_python = ">=3.7" summary = "A very fast and expressive template engine." groups = ["api"] @@ -411,8 +535,8 @@ dependencies = [ "MarkupSafe>=2.0", ] files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [[package]] @@ -433,29 +557,19 @@ files = [ ] [[package]] -name = "litestar" -version = "2.12.1" -requires_python = "<4.0,>=3.8" -summary = "Litestar - A production-ready, highly performant, extensible ASGI API Framework" +name = "loguru" +version = "0.7.3" +requires_python = "<4.0,>=3.5" +summary = "Python logging made (stupidly) simple" groups = ["default"] dependencies = [ - "anyio>=3", - "click", - "exceptiongroup; python_version < \"3.11\"", - "httpx>=0.22", - "importlib-metadata; python_version < \"3.10\"", - "importlib-resources>=5.12.0; python_version < \"3.9\"", - "msgspec>=0.18.2", - "multidict>=6.0.2", - "polyfactory>=2.6.3", - "pyyaml", - "rich-click", - "rich>=13.0.0", - "typing-extensions", + "aiocontextvars>=0.2.0; python_version < \"3.7\"", + "colorama>=0.3.4; sys_platform == \"win32\"", + "win32-setctime>=1.0.0; sys_platform == \"win32\"", ] files = [ - {file = "litestar-2.12.1-py3-none-any.whl", hash = "sha256:74915e3731c200caa099c416a1c3b3079ffacdd6e6393974e0284f8919606f9c"}, - {file = "litestar-2.12.1.tar.gz", hash = "sha256:d2cc43157060a06dac8a77e9dc6ba2936238beada61e272e8842c21fca23fcee"}, + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, ] [[package]] @@ -463,7 +577,7 @@ name = "markdown-it-py" version = "3.0.0" requires_python = ">=3.8" summary = "Python port of markdown-it. Markdown parsing, done right!" -groups = ["default", "api"] +groups = ["api"] dependencies = [ "mdurl~=0.1", ] @@ -497,7 +611,7 @@ name = "mdurl" version = "0.1.2" requires_python = ">=3.7" summary = "Markdown URL utilities" -groups = ["default", "api"] +groups = ["api"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -505,62 +619,16 @@ files = [ [[package]] name = "motor" -version = "3.6.0" -requires_python = ">=3.8" +version = "3.7.1" +requires_python = ">=3.9" summary = "Non-blocking MongoDB driver for Tornado or asyncio" groups = ["default"] dependencies = [ - "pymongo<4.10,>=4.9", + "pymongo<5.0,>=4.9", ] files = [ - {file = "motor-3.6.0-py3-none-any.whl", hash = "sha256:9f07ed96f1754963d4386944e1b52d403a5350c687edc60da487d66f98dbf894"}, - {file = "motor-3.6.0.tar.gz", hash = "sha256:0ef7f520213e852bf0eac306adf631aabe849227d8aec900a2612512fb9c5b8d"}, -] - -[[package]] -name = "msgspec" -version = "0.18.6" -requires_python = ">=3.8" -summary = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." -groups = ["default"] -files = [ - {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.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, -] - -[[package]] -name = "multidict" -version = "6.1.0" -requires_python = ">=3.8" -summary = "multidict implementation" -groups = ["default"] -dependencies = [ - "typing-extensions>=4.1.0; python_version < \"3.11\"", -] -files = [ - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, - {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, - {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, - {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, - {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, + {file = "motor-3.7.1-py3-none-any.whl", hash = "sha256:8a63b9049e38eeeb56b4fdd57c3312a6d1f25d01db717fe7d82222393c410298"}, + {file = "motor-3.7.1.tar.gz", hash = "sha256:27b4d46625c87928f331a6ca9d7c51c2f518ba0e270939d395bc1ddc89d64526"}, ] [[package]] @@ -576,22 +644,27 @@ files = [ [[package]] name = "orjson" -version = "3.10.11" -requires_python = ">=3.8" +version = "3.10.18" +requires_python = ">=3.9" summary = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" groups = ["default", "dev"] files = [ - {file = "orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51"}, - {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97"}, - {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19"}, - {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0"}, - {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433"}, - {file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5"}, - {file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd"}, - {file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b"}, - {file = "orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d"}, - {file = "orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284"}, - {file = "orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef"}, + {file = "orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753"}, + {file = "orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad"}, + {file = "orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06"}, + {file = "orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5"}, + {file = "orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e"}, + {file = "orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc"}, + {file = "orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a"}, + {file = "orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53"}, ] [[package]] @@ -607,29 +680,27 @@ files = [ [[package]] name = "pendulum" -version = "3.0.0" -requires_python = ">=3.8" +version = "3.1.0" +requires_python = ">=3.9" summary = "Python datetimes made easy" groups = ["default"] dependencies = [ - "backports-zoneinfo>=0.2.1; python_version < \"3.9\"", - "importlib-resources>=5.9.0; python_version < \"3.9\"", "python-dateutil>=2.6", - "time-machine>=2.6.0; implementation_name != \"pypy\"", "tzdata>=2020.1", ] files = [ - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, + {file = "pendulum-3.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4dfd53e7583ccae138be86d6c0a0b324c7547df2afcec1876943c4d481cf9608"}, + {file = "pendulum-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a6e06a28f3a7d696546347805536f6f38be458cb79de4f80754430696bea9e6"}, + {file = "pendulum-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e68d6a51880708084afd8958af42dc8c5e819a70a6c6ae903b1c4bfc61e0f25"}, + {file = "pendulum-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e3f1e5da39a7ea7119efda1dd96b529748c1566f8a983412d0908455d606942"}, + {file = "pendulum-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9af1e5eeddb4ebbe1b1c9afb9fd8077d73416ade42dd61264b3f3b87742e0bb"}, + {file = "pendulum-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f74aa8029a42e327bfc150472e0e4d2358fa5d795f70460160ba81b94b6945"}, + {file = "pendulum-3.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cf6229e5ee70c2660148523f46c472e677654d0097bec010d6730f08312a4931"}, + {file = "pendulum-3.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:350cabb23bf1aec7c7694b915d3030bff53a2ad4aeabc8c8c0d807c8194113d6"}, + {file = "pendulum-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:42959341e843077c41d47420f28c3631de054abd64da83f9b956519b5c7a06a7"}, + {file = "pendulum-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:006758e2125da2e624493324dfd5d7d1b02b0c44bc39358e18bf0f66d0767f5f"}, + {file = "pendulum-3.1.0-py3-none-any.whl", hash = "sha256:f9178c2a8e291758ade1e8dd6371b1d26d08371b4c7730a6e9a3ef8b16ebae0f"}, + {file = "pendulum-3.1.0.tar.gz", hash = "sha256:66f96303560f41d097bee7d2dc98ffca716fbb3a832c4b3062034c2d45865015"}, ] [[package]] @@ -654,24 +725,9 @@ files = [ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] -[[package]] -name = "polyfactory" -version = "2.17.0" -requires_python = "<4.0,>=3.8" -summary = "Mock data generation factories" -groups = ["default"] -dependencies = [ - "faker", - "typing-extensions>=4.6.0", -] -files = [ - {file = "polyfactory-2.17.0-py3-none-any.whl", hash = "sha256:71b677c17bb7cebad9a5631b1aca7718280bdcedc1c25278253717882d1ac294"}, - {file = "polyfactory-2.17.0.tar.gz", hash = "sha256:099d86f7c79c51a2caaf7c8598cc56e7b0a57c11b5918ddf699e82380735b6b7"}, -] - [[package]] name = "pre-commit" -version = "4.0.1" +version = "4.2.0" requires_python = ">=3.9" summary = "A framework for managing and maintaining multi-language pre-commit hooks." groups = ["dev"] @@ -683,8 +739,8 @@ dependencies = [ "virtualenv>=20.10.0", ] files = [ - {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, - {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, ] [[package]] @@ -704,12 +760,23 @@ files = [ {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, ] +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.9.2" requires_python = ">=3.8" summary = "Data validation using Python type hints" -groups = ["api"] +groups = ["default", "api"] dependencies = [ "annotated-types>=0.6.0", "pydantic-core==2.23.4", @@ -726,7 +793,7 @@ name = "pydantic-core" version = "2.23.4" requires_python = ">=3.8" summary = "Core functionality for Pydantic validation and serialization" -groups = ["api"] +groups = ["default", "api"] dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] @@ -746,12 +813,28 @@ files = [ {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] +[[package]] +name = "pydantic-settings" +version = "2.9.1" +requires_python = ">=3.9" +summary = "Settings management using Pydantic" +groups = ["default"] +dependencies = [ + "pydantic>=2.7.0", + "python-dotenv>=0.21.0", + "typing-inspection>=0.4.0", +] +files = [ + {file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"}, + {file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"}, +] + [[package]] name = "pygments" version = "2.18.0" requires_python = ">=3.8" summary = "Pygments is a syntax highlighting package written in Python." -groups = ["default", "api"] +groups = ["api"] files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, @@ -759,24 +842,24 @@ files = [ [[package]] name = "pymongo" -version = "4.9.2" -requires_python = ">=3.8" -summary = "Python driver for MongoDB " +version = "4.13.0" +requires_python = ">=3.9" +summary = "PyMongo - the Official MongoDB Python driver" groups = ["default"] dependencies = [ "dnspython<3.0.0,>=1.16.0", ] files = [ - {file = "pymongo-4.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8083bbe8cb10bb33dca4d93f8223dd8d848215250bb73867374650bac5fe69e1"}, - {file = "pymongo-4.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b8c636bf557c7166e3799bbf1120806ca39e3f06615b141c88d9c9ceae4d8c"}, - {file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aac5dce28454f47576063fbad31ea9789bba67cab86c95788f97aafd810e65b"}, - {file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1d5e7123af1fddf15b2b53e58f20bf5242884e671bcc3860f5e954fe13aeddd"}, - {file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe97c847b56d61e533a7af0334193d6b28375b9189effce93129c7e4733794a9"}, - {file = "pymongo-4.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ad54433a996e2d1985a9cd8fc82538ca8747c95caae2daf453600cc8c317f9"}, - {file = "pymongo-4.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98b9cade40f5b13e04492a42ae215c3721099be1014ddfe0fbd23f27e4f62c0c"}, - {file = "pymongo-4.9.2-cp312-cp312-win32.whl", hash = "sha256:dde6068ae7c62ea8ee2c5701f78c6a75618cada7e11f03893687df87709558de"}, - {file = "pymongo-4.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:e1ab6cd7cd2d38ffc7ccdc79fdc166c7a91a63f844a96e3e6b2079c054391c68"}, - {file = "pymongo-4.9.2.tar.gz", hash = "sha256:3e63535946f5df7848307b9031aa921f82bb0cbe45f9b0c3296f2173f9283eb0"}, + {file = "pymongo-4.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:007450b8c8d17b4e5b779ab6e1938983309eac26b5b8f0863c48effa4b151b07"}, + {file = "pymongo-4.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:899a5ea9cd32b1b0880015fdceaa36a41140a8c2ce8621626c52f7023724aed6"}, + {file = "pymongo-4.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b26cd4e090161927b7a81741a3627a41b74265dfb41c6957bfb474504b4b42"}, + {file = "pymongo-4.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b54e19e0f6c8a7ad0c5074a8cbefb29c12267c784ceb9a1577a62bbc43150161"}, + {file = "pymongo-4.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6208b83e7d566935218c0837f3b74c7d2dda83804d5d843ce21a55f22255ab74"}, + {file = "pymongo-4.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f33b8c1405d05517dce06756f2800b37dd098216cae5903cd80ad4f0a9dad08"}, + {file = "pymongo-4.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02f0e1af87280697a1a8304238b863d4eee98c8b97f554ee456c3041c0f3a021"}, + {file = "pymongo-4.13.0-cp312-cp312-win32.whl", hash = "sha256:5dea2f6b44697eda38a11ef754d2adfff5373c51b1ffda00b9fedc5facbd605f"}, + {file = "pymongo-4.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:c03e02129ad202d8e146480b398c4a3ea18266ee0754b6a4805de6baf4a6a8c7"}, + {file = "pymongo-4.13.0.tar.gz", hash = "sha256:92a06e3709e3c7e50820d352d3d4e60015406bcba69808937dac2a6d22226fde"}, ] [[package]] @@ -800,31 +883,47 @@ files = [ [[package]] name = "pytest-asyncio" -version = "0.24.0" -requires_python = ">=3.8" +version = "1.0.0" +requires_python = ">=3.9" summary = "Pytest support for asyncio" groups = ["dev"] dependencies = [ "pytest<9,>=8.2", + "typing-extensions>=4.12; python_version < \"3.10\"", ] files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3"}, + {file = "pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f"}, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +requires_python = ">=3.9" +summary = "Pytest plugin for measuring coverage." +groups = ["dev"] +dependencies = [ + "coverage[toml]>=7.5", + "pytest>=4.6", +] +files = [ + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, ] [[package]] name = "pytest-httpx" -version = "0.33.0" +version = "0.35.0" requires_python = ">=3.9" summary = "Send responses to httpx." groups = ["dev"] dependencies = [ - "httpx==0.27.*", + "httpx==0.28.*", "pytest==8.*", ] files = [ - {file = "pytest_httpx-0.33.0-py3-none-any.whl", hash = "sha256:bdd1b00a846cfe857194e4d3ba72dc08ba0d163154a4404269c9b971f357c05d"}, - {file = "pytest_httpx-0.33.0.tar.gz", hash = "sha256:4af9ab0dae5e9c14cb1e27d18af3db1f627b2cf3b11c02b34ddf26aff6b0a24c"}, + {file = "pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744"}, + {file = "pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f"}, ] [[package]] @@ -840,8 +939,8 @@ files = [ [[package]] name = "pytest-xdist" -version = "3.6.1" -requires_python = ">=3.8" +version = "3.7.0" +requires_python = ">=3.9" summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" groups = ["dev"] dependencies = [ @@ -849,24 +948,24 @@ dependencies = [ "pytest>=7.0.0", ] files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, + {file = "pytest_xdist-3.7.0-py3-none-any.whl", hash = "sha256:7d3fbd255998265052435eb9daa4e99b62e6fb9cfb6efd1f858d4d8c0c7f0ca0"}, + {file = "pytest_xdist-3.7.0.tar.gz", hash = "sha256:f9248c99a7c15b7d2f90715df93610353a485827bc06eefb6566d23f6400f126"}, ] [[package]] name = "pytest-xdist" -version = "3.6.1" +version = "3.7.0" extras = ["psutil"] -requires_python = ">=3.8" +requires_python = ">=3.9" summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" groups = ["dev"] dependencies = [ "psutil>=3.0", - "pytest-xdist==3.6.1", + "pytest-xdist==3.7.0", ] files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, + {file = "pytest_xdist-3.7.0-py3-none-any.whl", hash = "sha256:7d3fbd255998265052435eb9daa4e99b62e6fb9cfb6efd1f858d4d8c0c7f0ca0"}, + {file = "pytest_xdist-3.7.0.tar.gz", hash = "sha256:f9248c99a7c15b7d2f90715df93610353a485827bc06eefb6566d23f6400f126"}, ] [[package]] @@ -874,7 +973,7 @@ name = "python-dateutil" version = "2.9.0.post0" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Extensions to the standard Python datetime module" -groups = ["default"] +groups = ["default", "dev"] dependencies = [ "six>=1.5", ] @@ -888,7 +987,7 @@ name = "python-dotenv" version = "1.0.1" requires_python = ">=3.8" summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["api"] +groups = ["default", "api"] 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"}, @@ -896,13 +995,13 @@ files = [ [[package]] name = "python-multipart" -version = "0.0.17" +version = "0.0.20" requires_python = ">=3.8" summary = "A streaming multipart parser for Python" groups = ["api"] files = [ - {file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"}, - {file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"}, + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, ] [[package]] @@ -910,7 +1009,7 @@ name = "pyyaml" version = "6.0.2" requires_python = ">=3.8" summary = "YAML parser and emitter for Python" -groups = ["default", "api", "dev"] +groups = ["api", "dev"] files = [ {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, @@ -929,7 +1028,7 @@ name = "rich" version = "13.9.3" requires_python = ">=3.8.0" summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -groups = ["default", "api"] +groups = ["api"] dependencies = [ "markdown-it-py>=2.2.0", "pygments<3.0.0,>=2.13.0", @@ -940,23 +1039,6 @@ files = [ {file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"}, ] -[[package]] -name = "rich-click" -version = "1.8.3" -requires_python = ">=3.7" -summary = "Format click help output nicely with rich" -groups = ["default"] -dependencies = [ - "click>=7", - "importlib-metadata; python_version < \"3.8\"", - "rich>=10.7", - "typing-extensions", -] -files = [ - {file = "rich_click-1.8.3-py3-none-any.whl", hash = "sha256:636d9c040d31c5eee242201b5bf4f2d358bfae4db14bb22ec1cafa717cfd02cd"}, - {file = "rich_click-1.8.3.tar.gz", hash = "sha256:6d75bdfa7aa9ed2c467789a0688bc6da23fbe3a143e19aa6ad3f8bac113d2ab3"}, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -973,7 +1055,7 @@ name = "six" version = "1.16.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" summary = "Python 2 and 3 compatibility utilities" -groups = ["default"] +groups = ["default", "dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1015,6 +1097,17 @@ files = [ {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, ] +[[package]] +name = "soupsieve" +version = "2.7" +requires_python = ">=3.8" +summary = "A modern CSS selector implementation for Beautiful Soup." +groups = ["default"] +files = [ + {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, + {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, +] + [[package]] name = "starlette" version = "0.41.2" @@ -1030,31 +1123,6 @@ files = [ {file = "starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62"}, ] -[[package]] -name = "time-machine" -version = "2.16.0" -requires_python = ">=3.9" -summary = "Travel through time in your tests." -groups = ["default"] -marker = "implementation_name != \"pypy\"" -dependencies = [ - "python-dateutil", -] -files = [ - {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:84788f4d62a8b1bf5e499bb9b0e23ceceea21c415ad6030be6267ce3d639842f"}, - {file = "time_machine-2.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:15ec236b6571730236a193d9d6c11d472432fc6ab54e85eac1c16d98ddcd71bf"}, - {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cedc989717c8b44a3881ac3d68ab5a95820448796c550de6a2149ed1525157f0"}, - {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d26d79de1c63a8c6586c75967e09b0ff306aa7e944a1eaddb74595c9b1839ca"}, - {file = "time_machine-2.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317b68b56a9c3731e0cf8886e0f94230727159e375988b36c60edce0ddbcb44a"}, - {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e1e18279759897be3293a255d53e6b1cb0364b69d9591d0b80c51e461c94b0"}, - {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e43adb22def972a29d2b147999b56897116085777a0fea182fd93ee45730611e"}, - {file = "time_machine-2.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0c766bea27a0600e36806d628ebc4b47178b12fcdfb6c24dc0a566a9c06bfe7f"}, - {file = "time_machine-2.16.0-cp312-cp312-win32.whl", hash = "sha256:6dae82ab647d107817e013db82223e20a9853fa88543fec853ae326382d03c2e"}, - {file = "time_machine-2.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:265462c77dc9576267c3c7f20707780a171a9fdbac93ac22e608c309efd68c33"}, - {file = "time_machine-2.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:ef768e14768eebe3bb1196c0dece8e14c1c6991605721214a0c3c68cf77eb216"}, - {file = "time_machine-2.16.0.tar.gz", hash = "sha256:4a99acc273d2f98add23a89b94d4dd9e14969c01214c8514bfa78e4e9364c7e2"}, -] - [[package]] name = "typer" version = "0.13.0" @@ -1083,6 +1151,20 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +requires_python = ">=3.9" +summary = "Runtime typing introspection tools" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.12.0", +] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + [[package]] name = "tzdata" version = "2024.2" @@ -1234,6 +1316,18 @@ files = [ {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, ] +[[package]] +name = "win32-setctime" +version = "1.2.0" +requires_python = ">=3.5" +summary = "A small Python utility to set file creation time on Windows" +groups = ["default"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + [[package]] name = "wrapt" version = "1.16.0" diff --git a/freedium-library/pyproject.toml b/freedium-library/pyproject.toml index 01863a9..93a998c 100644 --- a/freedium-library/pyproject.toml +++ b/freedium-library/pyproject.toml @@ -6,14 +6,18 @@ authors = [ {name = "ZhymabekRoman", email = "robanokssamit@yandex.ru"}, ] dependencies = [ - "litestar>=2.12.1", "deprecation>=2.1.0", - "orjson>=3.10.11", - "pendulum>=3.0.0", + "orjson>=3.10.18", + "pendulum>=3.1.0", "ujson>=5.10.0", - "httpx[socks]>=0.27.2", - "pymongo>=4.9.2", - "motor>=3.6.0", + "httpx[socks]>=0.28.1", + "pymongo>=4.13.0", + "motor>=3.7.1", + "dependency-injector>=4.47.1", + "loguru>=0.7.3", + "pydantic-settings>=2.9.1", + "curl-cffi>=0.11.2", + "bs4>=0.0.2", ] requires-python = "==3.12.*" readme = "README.md" @@ -23,17 +27,20 @@ license = {text = "MIT"} [project.optional-dependencies] api = [ "slowapi>=0.1.9", - "fastapi[standard]>=0.115.5", + "fastapi[standard]>=0.115.12", ] [tool.pdm] distribution = false -[tool.pdm.dev-dependencies] + +[dependency-groups] dev = [ - "pytest-asyncio>=0.24.0", - "pytest-httpx>=0.33.0", - "pytest-xdist[psutil]>=3.6.1", - "orjson>=3.10.11", + "pytest-asyncio>=1.0.0", + "pytest-httpx>=0.35.0", + "pytest-xdist[psutil]>=3.7.0", + "orjson>=3.10.18", "pytest-integration>=0.2.3", - "pre-commit>=4.0.1", + "pre-commit>=4.2.0", + "freezegun>=1.5.2", + "pytest-cov>=6.1.1", ] diff --git a/freedium-library/pytest.ini b/freedium-library/pytest.ini new file mode 100644 index 0000000..d607527 --- /dev/null +++ b/freedium-library/pytest.ini @@ -0,0 +1,9 @@ +[pytest] +addopts = --cov=freedium_library +asyncio_default_fixture_loop_scope = function +filterwarnings = + ignore:Request should be used as a context manager using 'with' or 'async with' to ensure proper resource cleanup:UserWarning + +markers = + integration: marks tests as integration tests + medium_static: marks tests as medium static tests \ No newline at end of file diff --git a/freedium-library/src/freedium_library/api/app.py b/freedium-library/src/freedium_library/api/app.py index 965bd47..5d10936 100644 --- a/freedium-library/src/freedium_library/api/app.py +++ b/freedium-library/src/freedium_library/api/app.py @@ -7,6 +7,27 @@ from freedium_library.api.handlers import register_router from freedium_library.api.lifespan import lifespan from freedium_library.api.middlewares import register_middlewares from freedium_library.api.settings import ApplicationSettings +from fastapi.openapi.utils import get_openapi + + +def custom_openapi(app: FastAPI): + """ + Customize the OpenAPI schema to remove the HEAD method. + """ + if app.openapi_schema: + return app.openapi_schema + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + for path in openapi_schema.get("paths", {}): + if "head" in openapi_schema["paths"][path]: + del openapi_schema["paths"][path]["head"] + + app.openapi_schema = openapi_schema + return app.openapi_schema def create_application() -> FastAPI: @@ -32,6 +53,7 @@ def create_application() -> FastAPI: register_error_handler(app) register_middlewares(app) + app.openapi = lambda: custom_openapi(app) return app diff --git a/freedium-library/src/freedium_library/api/handlers/render.py b/freedium-library/src/freedium_library/api/handlers/render.py index b3d89c3..68af2cf 100644 --- a/freedium-library/src/freedium_library/api/handlers/render.py +++ b/freedium-library/src/freedium_library/api/handlers/render.py @@ -1,5 +1,4 @@ -from functools import partial - +from enum import Enum from fastapi import APIRouter from fastapi.responses import JSONResponse @@ -147,11 +146,16 @@ console.log('Hello, world!'); """ -async def render_page(service_name: str): +class ServiceName(str, Enum): + MEDIUM = "medium" + TWITTER = "twitter" + + +async def render_page(service_name: ServiceName): return JSONResponse( content={ "text": TEXT, - "service_name": service_name, + "service_name": service_name.value, "article": { "title": "UploadThing is 5x Faster", "date": "2024-09-13T12:00:00Z", @@ -182,13 +186,18 @@ async def render_page(service_name: str): def register_render_router(router: APIRouter) -> None: render_router = APIRouter(prefix="/services") - for service_name in ["medium", "twitter"]: + async def render_service_page(service_name: ServiceName): + return await render_page(service_name) + + for method in ["GET", "HEAD"]: render_router.add_api_route( - path=f"/{service_name}/render", - endpoint=partial(render_page, service_name), - summary=f"Render {service_name} page", - description=f"Render {service_name} page", - methods=["GET", "HEAD"], + "/{service_name}/render", + endpoint=render_service_page, + methods=[method], + summary="Render service page", + description="Render service page", + operation_id=f"render_page_{method}", + tags=["render"], ) router.include_router(render_router) diff --git a/freedium-library/src/freedium_library/api/settings.py b/freedium-library/src/freedium_library/api/settings.py index 8280ca4..7401318 100644 --- a/freedium-library/src/freedium_library/api/settings.py +++ b/freedium-library/src/freedium_library/api/settings.py @@ -31,7 +31,7 @@ class ApplicationSettings(BaseModel): prefix_path=prefix_path, openapi_url=f"{prefix_path}/openapi.json", docs_url=f"{prefix_path}/docs", - redoc_url=f"{prefix_path}/redoc", + redoc_url=f"{prefix_path}/redocs", **data, ) diff --git a/freedium-library/src/freedium_library/container.py b/freedium-library/src/freedium_library/container.py index 802f023..6adbd63 100644 --- a/freedium-library/src/freedium_library/container.py +++ b/freedium-library/src/freedium_library/container.py @@ -1,7 +1,7 @@ from dependency_injector import containers, providers -from freedium_library.utils.http import Request +from freedium_library.utils.http import HttpxRequest class Container(containers.DeclarativeContainer): - request = providers.Singleton(Request) + request = providers.Singleton(HttpxRequest) diff --git a/freedium-library/src/freedium_library/services/base.py b/freedium-library/src/freedium_library/services/base.py index 2731e28..c5f05b8 100644 --- a/freedium-library/src/freedium_library/services/base.py +++ b/freedium-library/src/freedium_library/services/base.py @@ -9,11 +9,11 @@ from loguru import logger from freedium_library.container import Container if TYPE_CHECKING: - from freedium_library.utils.http import Request + from freedium_library.utils.http import HttpxRequest class BaseService(ABC): - def __init__(self, request: Request = Provide[Container.request]): + def __init__(self, request: HttpxRequest = Provide[Container.request]): self.request = request def _prepare(self): @@ -60,11 +60,11 @@ class BaseService(ABC): pass @abstractmethod - async def _asearch(self, keywords: list[str]) -> list[dict]: + async def _asearch(self, keywords: list[str]) -> list[dict[str, str]]: pass @abstractmethod - def _search(self, keywords: list[str]) -> list[dict]: + def _search(self, keywords: list[str]) -> list[dict[str, str]]: pass def __str__(self) -> str: diff --git a/freedium-library/src/freedium_library/services/medium/api.py b/freedium-library/src/freedium_library/services/medium/api.py index b8f005f..622d1db 100644 --- a/freedium-library/src/freedium_library/services/medium/api.py +++ b/freedium-library/src/freedium_library/services/medium/api.py @@ -8,13 +8,13 @@ from freedium_library.utils import JSON, HashLib if TYPE_CHECKING: from freedium_library.services.medium.config import MediumConfig - from freedium_library.utils.http import Request + from freedium_library.utils.http import HttpxRequest class MediumApiService: def __init__( self, - request: Request, + request: HttpxRequest, config: MediumConfig, ): self.request = request diff --git a/freedium-library/src/freedium_library/services/medium/container.py b/freedium-library/src/freedium_library/services/medium/container.py index 1c0aaa1..e1aa69c 100644 --- a/freedium-library/src/freedium_library/services/medium/container.py +++ b/freedium-library/src/freedium_library/services/medium/container.py @@ -3,7 +3,7 @@ from dependency_injector import containers, providers from freedium_library.services.medium.validators import ( MediumServicePathValidator, ) -from freedium_library.utils.http import Request +from freedium_library.utils.http import HttpxRequest from .api import MediumApiService from .config import MediumConfig @@ -11,7 +11,7 @@ from .config import MediumConfig class MediumContainer(containers.DeclarativeContainer): config = providers.Singleton(MediumConfig) - request = providers.Singleton(Request) + request = providers.Singleton(HttpxRequest) api_service = providers.Singleton( MediumApiService, request=request, diff --git a/freedium-library/src/freedium_library/services/medium/medium.py b/freedium-library/src/freedium_library/services/medium/medium.py index 65065ea..88849c7 100644 --- a/freedium-library/src/freedium_library/services/medium/medium.py +++ b/freedium-library/src/freedium_library/services/medium/medium.py @@ -14,13 +14,13 @@ from .models import MediumPostDataResponse from .validators import MediumServicePathValidator if TYPE_CHECKING: - from freedium_library.utils.http import Request + from freedium_library.utils.http import HttpxRequest class MediumService(BaseService): def __init__( self, - request: Request = Provide[Container.request], + request: HttpxRequest = Provide[Container.request], api_service: MediumApiService = Provide[MediumContainer.api_service], path_validator: MediumServicePathValidator = Provide[MediumContainer.validator], ): diff --git a/freedium-library/src/freedium_library/services/medium/static/medium_domains.txt b/freedium-library/src/freedium_library/services/medium/static/medium_domains.txt index f3e851a..2bdf321 100644 --- a/freedium-library/src/freedium_library/services/medium/static/medium_domains.txt +++ b/freedium-library/src/freedium_library/services/medium/static/medium_domains.txt @@ -6,7 +6,6 @@ drlee.io generativeai.pub towardsdev.com infosecwriteups.com -towardsdatascience.com thetaoist.online devopsquare.com bettermarketing.pub @@ -29,5 +28,4 @@ blog.stackademic.com ai.gopubby.com blog.devops.dev levelup.gitconnected.com -betterhumans.coach.me ai.plainenglish.io diff --git a/freedium-library/src/freedium_library/services/medium/static/non_active_medium_domains.txt b/freedium-library/src/freedium_library/services/medium/static/non_active_medium_domains.txt index 71f361c..9ffaca8 100644 --- a/freedium-library/src/freedium_library/services/medium/static/non_active_medium_domains.txt +++ b/freedium-library/src/freedium_library/services/medium/static/non_active_medium_domains.txt @@ -1 +1,3 @@ productcoalition.com +betterhumans.coach.me +towardsdatascience.com \ No newline at end of file diff --git a/freedium-library/src/freedium_library/services/medium/static/tests/test_static.py b/freedium-library/src/freedium_library/services/medium/static/tests/test_static.py index 4946266..dd368ee 100644 --- a/freedium-library/src/freedium_library/services/medium/static/tests/test_static.py +++ b/freedium-library/src/freedium_library/services/medium/static/tests/test_static.py @@ -47,7 +47,7 @@ def get_file_path(filename: str) -> str: return os.path.join(os.path.dirname(os.path.abspath(__file__)), "../", filename) -@pytest.mark.integration +@pytest.mark.medium_static @pytest.mark.parametrize( "domain", get_domains_from_file(get_file_path("medium_domains.txt")), @@ -60,7 +60,7 @@ def test_medium_domain_meta_tag(domain: str): logger.success(f"Test passed for domain: {domain}") -@pytest.mark.integration +@pytest.mark.medium_static @pytest.mark.parametrize( "domain", get_domains_from_file(get_file_path("non_active_medium_domains.txt")), @@ -68,7 +68,7 @@ def test_medium_domain_meta_tag(domain: str): def test_non_active_medium_domain(domain: str): logger.info(f"Starting test for non-active domain: {domain}") result = check_medium_meta_tag(domain) - assert ( - result is False or result is None - ), f"Domain {domain} should be non-active but appears to be active" + assert result is False or result is None, ( + f"Domain {domain} should be non-active but appears to be active" + ) logger.success(f"Test passed for non-active domain: {domain}") diff --git a/freedium-library/src/freedium_library/services/medium/tests/validators_url_test.py b/freedium-library/src/freedium_library/services/medium/tests/validators_url_test.py index 2c0de16..a387c84 100644 --- a/freedium-library/src/freedium_library/services/medium/tests/validators_url_test.py +++ b/freedium-library/src/freedium_library/services/medium/tests/validators_url_test.py @@ -8,7 +8,7 @@ from freedium_library.services.medium.validators import ( _MediumServiceHashesValidator, # type: ignore _MediumServiceURLValidator, # type: ignore ) -from freedium_library.utils.http import Request +from freedium_library.utils.http import HttpxRequest @pytest.fixture @@ -114,20 +114,19 @@ async def test_resolve_medium_url_with_real_short_link( assert "rsci.app.link/vYe3nWA8wBb" in mock_request.aget.call_args[0][0] -@pytest.mark.integration @pytest.mark.asyncio async def test_resolve_medium_url_with_real_short_link_integration() -> None: - request = Request() - config = MediumConfig() - api_service = MediumApiService(request=request, config=config) - hash_validator = _MediumServiceHashesValidator() - url_validator = _MediumServiceURLValidator(api_service, hash_validator, request) + async with HttpxRequest() as request: + config = MediumConfig() + api_service = MediumApiService(request=request, config=config) + hash_validator = _MediumServiceHashesValidator() + url_validator = _MediumServiceURLValidator(api_service, hash_validator, request) - result = await url_validator.resolve_medium_url( - "https://link.medium.com/vYe3nWA8wBb" - ) + result = await url_validator.resolve_medium_url( + "https://link.medium.com/vYe3nWA8wBb" + ) - assert result == "77ae792a1a43" + assert result == "77ae792a1a43" @pytest.mark.asyncio diff --git a/freedium-library/src/freedium_library/services/medium/validators.py b/freedium-library/src/freedium_library/services/medium/validators.py index 50f4860..4ca895c 100644 --- a/freedium-library/src/freedium_library/services/medium/validators.py +++ b/freedium-library/src/freedium_library/services/medium/validators.py @@ -13,7 +13,7 @@ from freedium_library.container import Container from freedium_library.utils.http import URLProcessor if TYPE_CHECKING: - from freedium_library.utils.http import Request + from freedium_library.utils.http import HttpxRequest from .api import MediumApiService @@ -45,7 +45,7 @@ class _MediumServiceURLValidator: self, api_service: MediumApiService, hash_validator: _MediumServiceHashesValidator, - request: Request = Provide[Container.request], + request: HttpxRequest = Provide[Container.request], ): self.api_service = api_service self.request = request diff --git a/freedium-library/src/freedium_library/utils/http/__init__.py b/freedium-library/src/freedium_library/utils/http/__init__.py index cbda7b3..5ef0212 100644 --- a/freedium-library/src/freedium_library/utils/http/__init__.py +++ b/freedium-library/src/freedium_library/utils/http/__init__.py @@ -1,4 +1,21 @@ -from .client import Request, RequestConfig +from .client import ( + AbstractRequest, + AbstractResponse, + CurlRequest, + CurlResponse, + HttpxRequest, + HttpxResponse, + RequestConfig, +) from .url import URLProcessor -__all__ = ["Request", "URLProcessor", "RequestConfig"] +__all__ = [ + "AbstractRequest", + "AbstractResponse", + "CurlRequest", + "CurlResponse", + "HttpxRequest", + "HttpxResponse", + "URLProcessor", + "RequestConfig", +] diff --git a/freedium-library/src/freedium_library/utils/http/client/__init__.py b/freedium-library/src/freedium_library/utils/http/client/__init__.py index 65f38d1..5ad3587 100644 --- a/freedium-library/src/freedium_library/utils/http/client/__init__.py +++ b/freedium-library/src/freedium_library/utils/http/client/__init__.py @@ -1,4 +1,17 @@ -from .client import Request +from .abstract import AbstractRequest from .config import RequestConfig +from .curl import CurlRequest +from .curl_response import CurlResponse +from .httpx import HttpxRequest +from .httpx_response import HttpxResponse +from .response import AbstractResponse -__all__ = ["Request", "RequestConfig"] +__all__ = ( + "AbstractRequest", + "AbstractResponse", + "HttpxRequest", + "HttpxResponse", + "CurlRequest", + "CurlResponse", + "RequestConfig", +) diff --git a/freedium-library/src/freedium_library/utils/http/client/abstract.py b/freedium-library/src/freedium_library/utils/http/client/abstract.py new file mode 100644 index 0000000..86d492b --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/abstract.py @@ -0,0 +1,127 @@ +import abc +from types import TracebackType +from typing import Any, Dict, Optional, Type, TypeVar + +from .response import AbstractResponse + +T = TypeVar("T", bound="AbstractRequest") + + +class AbstractRequest(abc.ABC): + """Abstract base class for HTTP request clients.""" + + @abc.abstractmethod + def __enter__(self) -> "AbstractRequest": + """Enter the context manager.""" + pass + + @abc.abstractmethod + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + """Exit the context manager.""" + pass + + @abc.abstractmethod + async def __aenter__(self) -> "AbstractRequest": + """Enter the async context manager.""" + pass + + @abc.abstractmethod + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + """Exit the async context manager.""" + pass + + @abc.abstractmethod + def get( + self, + url: str, + params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform a GET request.""" + pass + + @abc.abstractmethod + def post( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform a POST request.""" + pass + + @abc.abstractmethod + def put( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform a PUT request.""" + pass + + @abc.abstractmethod + def delete( + self, + url: str, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform a DELETE request.""" + pass + + @abc.abstractmethod + async def aget( + self, + url: str, + params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform an async GET request.""" + pass + + @abc.abstractmethod + async def apost( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform an async POST request.""" + pass + + @abc.abstractmethod + async def aput( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform an async PUT request.""" + pass + + @abc.abstractmethod + async def adelete( + self, + url: str, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + """Perform an async DELETE request.""" + pass diff --git a/freedium-library/src/freedium_library/utils/http/client/curl.py b/freedium-library/src/freedium_library/utils/http/client/curl.py new file mode 100644 index 0000000..deb4254 --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/curl.py @@ -0,0 +1,218 @@ +import warnings +from types import TracebackType +from typing import Any, Dict, Optional, Type + +from curl_cffi.requests import Session, AsyncSession + +from .abstract import AbstractRequest +from .config import RequestConfig +from .curl_response import CurlResponse +from .response import AbstractResponse + + +class CurlRequest(AbstractRequest): + __slots__ = ("config", "_in_context_manager", "_session", "_async_session") + + def __init__(self, config: Optional[RequestConfig] = None): + self.config = config or RequestConfig() + self._in_context_manager = False + self._session: Any = None + self._async_session: Any = None + warnings.warn( + "Request should be used as a context manager using 'with' or 'async with' " + "to ensure proper resource cleanup", + stacklevel=2, + ) + + def _get_session(self) -> Any: + if not self._session: + self._session = Session(impersonate="chrome110") + return self._session + + async def _get_async_session(self) -> Any: + if not self._async_session: + self._async_session = AsyncSession(impersonate="chrome110") + return self._async_session + + def __enter__(self) -> "CurlRequest": + self._in_context_manager = True + self._session = Session(impersonate="chrome110") + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + if self._session: + self._session.close() + self._session = None + + async def __aenter__(self) -> "CurlRequest": + self._in_context_manager = True + self._async_session = AsyncSession(impersonate="chrome110") + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + if self._async_session: + await self._async_session.close() + self._async_session = None + + def __del__(self): + if self._session: + self._session.close() + # Can't handle async session cleanup in __del__ + + def _check_context_manager(self): + if not self._in_context_manager: + warnings.warn( + "Request is not being used as a context manager. This may lead to " + "resource leaks. Use 'with' or 'async with' statement.", + stacklevel=2, + ) + + def get( + self, + url: str, + params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = self._get_session() + response = session.get( + url, + params=params, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) + + def post( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = self._get_session() + response = session.post( + url, + json=data, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) + + def put( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = self._get_session() + response = session.put( + url, + json=data, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) + + def delete( + self, + url: str, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = self._get_session() + response = session.delete( + url, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) + + async def aget( + self, + url: str, + params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = await self._get_async_session() + response = await session.get( + url, + params=params, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) + + async def apost( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = await self._get_async_session() + response = await session.post( + url, + json=data, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) + + async def aput( + self, + url: str, + data: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = await self._get_async_session() + response = await session.put( + url, + json=data, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) + + async def adelete( + self, + url: str, + headers: Optional[Dict[str, str]] = None, + follow_redirects: bool = True, + ) -> AbstractResponse: + self._check_context_manager() + session = await self._get_async_session() + response = await session.delete( + url, + headers=headers, + follow=follow_redirects, + timeout=self.config.timeout, + ) + return CurlResponse(response) diff --git a/freedium-library/src/freedium_library/utils/http/client/curl_response.py b/freedium-library/src/freedium_library/utils/http/client/curl_response.py new file mode 100644 index 0000000..d40f323 --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/curl_response.py @@ -0,0 +1,94 @@ +from typing import ( + Any, + AsyncGenerator, + AsyncIterator, + Coroutine, + Generator, + Iterator, + Optional, + cast, +) + +from curl_cffi.requests import Response as CurlCffiResponse + +from .headers import Headers +from .response import AbstractResponse + + +class CurlResponse(AbstractResponse): + """Wrapper for curl_cffi's Response class that implements AbstractResponse.""" + + def __init__(self, response: CurlCffiResponse): + self._response = response + self._headers: Optional[Headers] = None + + @property + def status_code(self) -> int: + return self._response.status_code + + @property + def headers(self) -> Headers: + if self._headers is None: + headers_dict = { + k: v if v is not None else "" for k, v in self._response.headers.items() + } + self._headers = Headers(headers_dict) + return self._headers + + @property + def content(self) -> bytes: + return self._response.content + + @property + def text(self) -> str: + return self._response.text + + def json(self, **kwargs: Any) -> Any: + result: Any = self._response.json(**kwargs) # type: ignore + return result + + @property + def url(self) -> str: + return str(self._response.url) + + @property + def is_success(self) -> bool: + return 200 <= self.status_code < 300 + + @property + def is_redirect(self) -> bool: + return 300 <= self.status_code < 400 + + @property + def is_error(self) -> bool: + return self.status_code >= 400 + + def raise_for_status(self) -> None: + self._response.raise_for_status() + + def iter_content(self, chunk_size: Optional[int] = None) -> Iterator[bytes]: + def _iter_content() -> Generator[bytes, Any, None]: + for chunk in self._response.iter_content( + chunk_size=chunk_size, decode_unicode=False + ): # type: ignore + yield cast(bytes, chunk) + + return _iter_content() + + def aiter_content( + self, chunk_size: Optional[int] = None + ) -> Coroutine[Any, Any, AsyncIterator[bytes]]: + async def _aiter_content() -> AsyncGenerator[bytes, Any]: + async for chunk in self._response.aiter_content( + chunk_size=chunk_size, decode_unicode=False + ): # type: ignore + yield cast(bytes, chunk) + + return cast(Coroutine[Any, Any, AsyncIterator[bytes]], _aiter_content()) + + def close(self) -> None: + self._response.close() + + def __getattr__(self, name: str) -> Any: + """Delegate any other attributes to the underlying response object.""" + return getattr(self._response, name) diff --git a/freedium-library/src/freedium_library/utils/http/client/headers.py b/freedium-library/src/freedium_library/utils/http/client/headers.py new file mode 100644 index 0000000..0a723d0 --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/headers.py @@ -0,0 +1,70 @@ +import abc +from collections.abc import Iterator, Mapping +from typing import Any, Dict, List, Tuple, Union, cast + + +class AbstractHeaders(abc.ABC, Mapping[str, str]): + """Abstract base class for HTTP headers.""" + + @abc.abstractmethod + def __getitem__(self, key: str) -> str: + """Get a header value by name (case-insensitive).""" + pass + + @abc.abstractmethod + def __iter__(self) -> Iterator[str]: + """Iterate over header names.""" + pass + + @abc.abstractmethod + def __len__(self) -> int: + """Get the number of unique headers.""" + pass + + +class Headers(AbstractHeaders): + """ + A case-insensitive dictionary-like object for HTTP headers. + + It stores header keys in a case-insensitive manner. + If multiple headers with the same name are provided, the last one is kept. + """ + + def __init__( + self, + headers: Union[ + Mapping[str, str], "Headers", List[Tuple[str, str]], None + ] = None, + ): + self._headers: Dict[str, Tuple[str, str]] = {} + if headers: + if isinstance(headers, Mapping): + for key, value in headers.items(): + self._headers[key.lower()] = (key, str(value)) + else: + for key, value in headers: + self._headers[key.lower()] = (key, str(value)) + + def __getitem__(self, key: str) -> str: + return self._headers[key.lower()][1] + + def __iter__(self) -> Iterator[str]: + return (original_key for original_key, _ in self._headers.values()) + + def __len__(self) -> int: + return len(self._headers) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Mapping): + return NotImplemented + + self_lower: Dict[str, str] = {} + for k, v in cast(Iterator[Tuple[str, str]], self.items()): + self_lower[k.lower()] = v + other_lower: Dict[str, str] = {} + for k, v in cast(Iterator[Tuple[str, str]], other.items()): + other_lower[k.lower()] = v + return self_lower == other_lower + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({dict(self.items())!r})" diff --git a/freedium-library/src/freedium_library/utils/http/client/client.py b/freedium-library/src/freedium_library/utils/http/client/httpx.py similarity index 75% rename from freedium-library/src/freedium_library/utils/http/client/client.py rename to freedium-library/src/freedium_library/utils/http/client/httpx.py index b78d094..430c959 100644 --- a/freedium-library/src/freedium_library/utils/http/client/client.py +++ b/freedium-library/src/freedium_library/utils/http/client/httpx.py @@ -1,5 +1,6 @@ import warnings -from typing import Any, Dict, Optional +from types import TracebackType +from typing import Any, Dict, Optional, Type from httpx import ( AsyncClient, @@ -10,11 +11,14 @@ from httpx import ( Timeout, ) +from .abstract import AbstractRequest from .config import RequestConfig +from .httpx_response import HttpxResponse +from .response import AbstractResponse # https://github.com/encode/httpx/discussions/1748 -class Request: +class HttpxRequest(AbstractRequest): __slots__ = ("config", "_in_context_manager") def __init__(self, config: Optional[RequestConfig] = None): @@ -38,7 +42,7 @@ class Request: def _client(self) -> Client: return Client( transport=self._transport, - proxies=self.proxy_url, + proxy=self.proxy_url, ) @property @@ -49,7 +53,7 @@ class Request: def _async_client(self) -> AsyncClient: return AsyncClient( transport=self._async_transport, - proxies=self.proxy_url, + proxy=self.proxy_url, ) @property @@ -57,18 +61,28 @@ class Request: timeout = Timeout(timeout=self.config.timeout) return timeout - def __enter__(self): + def __enter__(self) -> "HttpxRequest": self._in_context_manager = True return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: self._client.close() - async def __aenter__(self): + async def __aenter__(self) -> "HttpxRequest": self._in_context_manager = True return self - async def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: await self._async_client.aclose() def __del__(self): @@ -89,15 +103,16 @@ class Request: params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return self._client.get( + response = self._client.get( url, params=params, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) def post( self, @@ -105,15 +120,16 @@ class Request: data: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return self._client.post( + response = self._client.post( url, json=data, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) def put( self, @@ -121,29 +137,31 @@ class Request: data: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return self._client.put( + response = self._client.put( url, json=data, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) def delete( self, url: str, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return self._client.delete( + response = self._client.delete( url, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) async def aget( self, @@ -151,15 +169,16 @@ class Request: params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return await self._async_client.get( + response = await self._async_client.get( url, params=params, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) async def apost( self, @@ -167,15 +186,16 @@ class Request: data: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return await self._async_client.post( + response = await self._async_client.post( url, json=data, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) async def aput( self, @@ -183,26 +203,28 @@ class Request: data: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return await self._async_client.put( + response = await self._async_client.put( url, json=data, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) async def adelete( self, url: str, headers: Optional[Dict[str, str]] = None, follow_redirects: bool = True, - ) -> Response: + ) -> AbstractResponse: self._check_context_manager() - return await self._async_client.delete( + response = await self._async_client.delete( url, headers=headers, follow_redirects=follow_redirects, timeout=self._timeout_client, ) + return HttpxResponse(response) diff --git a/freedium-library/src/freedium_library/utils/http/client/httpx_response.py b/freedium-library/src/freedium_library/utils/http/client/httpx_response.py new file mode 100644 index 0000000..f6bdff6 --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/httpx_response.py @@ -0,0 +1,69 @@ +from typing import Any, AsyncIterator, Iterator, Optional + +from httpx import Response as HttpxNativeResponse + +from .headers import Headers +from .response import AbstractResponse + + +class HttpxResponse(AbstractResponse): + """Wrapper for httpx's Response class that implements AbstractResponse.""" + + def __init__(self, response: HttpxNativeResponse): + self._response = response + self._headers: Optional[Headers] = None + + @property + def status_code(self) -> int: + return self._response.status_code + + @property + def headers(self) -> Headers: + if self._headers is None: + self._headers = Headers(self._response.headers) + return self._headers + + @property + def content(self) -> bytes: + return self._response.content + + @property + def text(self) -> str: + return self._response.text + + def json(self) -> Any: + return self._response.json() + + @property + def url(self) -> str: + return str(self._response.url) + + @property + def is_success(self) -> bool: + return self._response.is_success + + @property + def is_redirect(self) -> bool: + return self._response.is_redirect + + @property + def is_error(self) -> bool: + return self._response.is_error + + def raise_for_status(self) -> None: + self._response.raise_for_status() + + def iter_content(self, chunk_size: Optional[int] = None) -> Iterator[bytes]: + return self._response.iter_bytes(chunk_size) + + async def aiter_content( + self, chunk_size: Optional[int] = None + ) -> AsyncIterator[bytes]: + return self._response.aiter_bytes(chunk_size) + + def close(self) -> None: + self._response.close() + + def __getattr__(self, name: str) -> Any: + """Delegate any other attributes to the underlying response object.""" + return getattr(self._response, name) diff --git a/freedium-library/src/freedium_library/utils/http/client/response.py b/freedium-library/src/freedium_library/utils/http/client/response.py new file mode 100644 index 0000000..00cca16 --- /dev/null +++ b/freedium-library/src/freedium_library/utils/http/client/response.py @@ -0,0 +1,83 @@ +import abc +from typing import Any, AsyncIterator, Iterator, Optional + +from .headers import AbstractHeaders + + +class AbstractResponse(abc.ABC): + """Abstract base class for HTTP responses.""" + + @property + @abc.abstractmethod + def status_code(self) -> int: + """HTTP status code.""" + pass + + @property + @abc.abstractmethod + def headers(self) -> AbstractHeaders: + """HTTP response headers.""" + pass + + @property + @abc.abstractmethod + def content(self) -> bytes: + """Raw response content as bytes.""" + pass + + @property + @abc.abstractmethod + def text(self) -> str: + """Response content as text.""" + pass + + @abc.abstractmethod + def json(self) -> Any: + """Parse response content as JSON.""" + pass + + @property + @abc.abstractmethod + def url(self) -> str: + """Final URL of the response.""" + pass + + @property + @abc.abstractmethod + def is_success(self) -> bool: + """Whether the request was successful (status code 2xx).""" + pass + + @property + @abc.abstractmethod + def is_redirect(self) -> bool: + """Whether the response is a redirect.""" + pass + + @property + @abc.abstractmethod + def is_error(self) -> bool: + """Whether the response is an error (status code 4xx or 5xx).""" + pass + + @abc.abstractmethod + def raise_for_status(self) -> None: + """Raise an exception if the response has an error status code.""" + pass + + @abc.abstractmethod + def iter_content(self, chunk_size: Optional[int] = None) -> Iterator[bytes]: + """Iterate over the response content in chunks.""" + pass + + @abc.abstractmethod + async def aiter_content( + self, chunk_size: Optional[int] = None + ) -> AsyncIterator[bytes]: + """Async iterate over the response content in chunks.""" + pass + + @abc.abstractmethod + def close(self) -> None: + """Close the response and release resources.""" + pass diff --git a/freedium-library/src/freedium_library/utils/http/client/tests/client_test.py b/freedium-library/src/freedium_library/utils/http/client/tests/client_test.py index b7f19c0..1cd6d13 100644 --- a/freedium-library/src/freedium_library/utils/http/client/tests/client_test.py +++ b/freedium-library/src/freedium_library/utils/http/client/tests/client_test.py @@ -1,9 +1,10 @@ -from typing import Any, Literal, Optional +from typing import Any, Dict, Literal, Optional, cast import pytest +from httpx import Request as HttpxNativeRequest from pytest_httpx import HTTPXMock -from freedium_library.utils.http.client import Request, RequestConfig +from freedium_library.utils.http.client import HttpxRequest, RequestConfig @pytest.fixture @@ -14,10 +15,11 @@ def mock_response() -> dict[str, Any]: def test_sync_context_manager(httpx_mock: HTTPXMock, mock_response: dict[str, Any]): httpx_mock.add_response(**mock_response) - with Request() as client: + with HttpxRequest() as client: response = client.get("http://test.com") assert response.status_code == 200 - assert client._in_context_manager is True + # Access internal attribute for testing + assert getattr(client, "_in_context_manager") is True def test_sync_without_context_manager( @@ -26,14 +28,15 @@ def test_sync_without_context_manager( httpx_mock.add_response(**mock_response) with pytest.warns(UserWarning, match="Request should be used as a context manager"): - client = Request() + client = HttpxRequest() with pytest.warns( UserWarning, match="Request is not being used as a context manager" ): response = client.get("http://test.com") assert response.status_code == 200 - assert client._in_context_manager is False + # Access internal attribute for testing + assert getattr(client, "_in_context_manager") is False @pytest.mark.asyncio @@ -42,10 +45,11 @@ async def test_async_context_manager( ): httpx_mock.add_response(**mock_response) - async with Request() as client: + async with HttpxRequest() as client: response = await client.aget("http://test.com") assert response.status_code == 200 - assert client._in_context_manager is True + # Access internal attribute for testing + assert getattr(client, "_in_context_manager") is True @pytest.mark.asyncio @@ -55,14 +59,15 @@ async def test_async_without_context_manager( httpx_mock.add_response(**mock_response) with pytest.warns(UserWarning, match="Request should be used as a context manager"): - client = Request() + client = HttpxRequest() with pytest.warns( UserWarning, match="Request is not being used as a context manager" ): response = await client.aget("http://test.com") assert response.status_code == 200 - assert client._in_context_manager is False + # Access internal attribute for testing + assert getattr(client, "_in_context_manager") is False @pytest.mark.parametrize( @@ -74,21 +79,25 @@ async def test_async_without_context_manager( ("delete", "adelete", None), ], ) -def test_http_methods( +@pytest.mark.asyncio +async def test_http_methods( httpx_mock: HTTPXMock, mock_response: dict[str, Any], method: Literal["get", "post", "put", "delete"], async_method: Literal["aget", "apost", "aput", "adelete"], - data: Optional[dict[Any, Any]], + data: Optional[Dict[str, Any]], ): httpx_mock.add_response(**mock_response) - with Request() as client: - func = getattr(client, method) - kwargs = {"url": "http://test.com"} + async with HttpxRequest() as client: + func = getattr(client, async_method) + kwargs: Dict[str, Any] = {"url": "http://test.com"} if data: - kwargs["data"] = data - response = func(**kwargs) + if async_method in ["apost", "aput"]: + kwargs["data"] = data + else: + kwargs["params"] = data + response = await func(**kwargs) assert response.status_code == 200 @@ -107,252 +116,235 @@ async def test_async_http_methods( mock_response: dict[str, Any], method: Literal["get", "post", "put", "delete"], async_method: Literal["aget", "apost", "aput", "adelete"], - data: Optional[dict[Any, Any]], + data: Optional[Dict[str, Any]], ): httpx_mock.add_response(**mock_response) - async with Request() as client: + async with HttpxRequest() as client: func = getattr(client, async_method) - kwargs = {"url": "http://test.com"} + kwargs: Dict[str, Any] = {"url": "http://test.com"} if data: - kwargs["data"] = data + if async_method in ["apost", "aput"]: + kwargs["data"] = data + else: + kwargs["params"] = data response = await func(**kwargs) assert response.status_code == 200 -def test_custom_config(): - config = RequestConfig(timeout=20, retries=5) # , backoff_factor=0.2 - client = Request(config) - assert client.config.timeout == 20 - assert client.config.retries == 5 - # assert client.config.backoff_factor == 0.2 +@pytest.mark.asyncio +async def test_custom_config(): + config = RequestConfig(timeout=20, retries=5) + async with HttpxRequest(config) as client: + assert client.config.timeout == 20 + assert client.config.retries == 5 @pytest.mark.parametrize("context_manager", [True, False]) -def test_resource_cleanup(context_manager): +def test_resource_cleanup(context_manager: bool): if context_manager: - with Request() as client: + with HttpxRequest() as client: pass else: - client = Request() + with pytest.warns( + UserWarning, match="Request should be used as a context manager" + ): + client = HttpxRequest() del client @pytest.fixture def request_client(): - return Request() + return HttpxRequest() @pytest.fixture def custom_config(): - return RequestConfig(timeout=5, retries=2) # , backoff_factor=0.2 + return RequestConfig(timeout=5, retries=2) def test_request_config_defaults(): config = RequestConfig() assert config.timeout == 6 assert config.retries == 3 - # assert config.backoff_factor == 0.1 -def test_request_custom_config(): - config = RequestConfig(timeout=5, retries=2) # , backoff_factor=0.2 - request = Request(config=config) - assert request.config.timeout == 5 - assert request.config.retries == 2 - # assert request.config.backoff_factor == 0.2 +@pytest.mark.asyncio +async def test_request_custom_config(custom_config: RequestConfig): + async with HttpxRequest(config=custom_config) as request: + assert request.config.timeout == 5 + assert request.config.retries == 2 -def test_get_request(request_client: Request, httpx_mock: HTTPXMock): +@pytest.mark.asyncio +async def test_get_request(httpx_mock: HTTPXMock): url = "https://api.example.com/data" expected_response = {"key": "value"} httpx_mock.add_response(json=expected_response) - response = request_client.get(url) - assert response.json() == expected_response - request = httpx_mock.get_request() - assert request.url == url + async with HttpxRequest() as request_client: + response = await request_client.aget(url) + assert response.json() == expected_response + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url -def test_post_request(request_client: Request, httpx_mock: HTTPXMock): +@pytest.mark.asyncio +async def test_post_request(httpx_mock: HTTPXMock): url = "https://api.example.com/data" data = {"test": "data"} httpx_mock.add_response(json={"status": "success"}) - response = request_client.post(url, data=data) - request = httpx_mock.get_request() - assert request.url == url - assert request.read().decode() == '{"test": "data"}' + async with HttpxRequest() as request_client: + response = await request_client.apost(url, data=data) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url + assert request.read().decode() == '{"test":"data"}' -def test_put_request(request_client: Request, httpx_mock: HTTPXMock): +@pytest.mark.asyncio +async def test_put_request(httpx_mock: HTTPXMock): url = "https://api.example.com/data" data = {"test": "data"} httpx_mock.add_response(json={"status": "updated"}) - response = request_client.put(url, data=data) - request = httpx_mock.get_request() - assert request.url == url - assert request.read().decode() == '{"test": "data"}' - - -def test_delete_request(request_client: Request, httpx_mock: HTTPXMock): - url = "https://api.example.com/data" - httpx_mock.add_response(json={"status": "deleted"}) - - response = request_client.delete(url) - request = httpx_mock.get_request() - assert request.url == url + async with HttpxRequest() as request_client: + response = await request_client.aput(url, data=data) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url + assert request.read().decode() == '{"test":"data"}' @pytest.mark.asyncio -async def test_aget_request(request_client: Request, httpx_mock: HTTPXMock): +async def test_delete_request(httpx_mock: HTTPXMock): + url = "https://api.example.com/data" + httpx_mock.add_response(json={"status": "deleted"}) + + async with HttpxRequest() as request_client: + response = await request_client.adelete(url) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url + + +@pytest.mark.asyncio +async def test_aget_request(httpx_mock: HTTPXMock): url = "https://api.example.com/data" expected_response = {"key": "value"} httpx_mock.add_response(json=expected_response) - response = await request_client.aget(url) - assert response.json() == expected_response - request = httpx_mock.get_request() - assert request.url == url + async with HttpxRequest() as request_client: + response = await request_client.aget(url) + assert response.json() == expected_response + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url @pytest.mark.asyncio -async def test_apost_request(request_client: Request, httpx_mock: HTTPXMock): +async def test_apost_request(httpx_mock: HTTPXMock): url = "https://api.example.com/data" data = {"test": "data"} httpx_mock.add_response(json={"status": "success"}) - response = await request_client.apost(url, data=data) - request = httpx_mock.get_request() - assert request.url == url - assert request.read().decode() == '{"test": "data"}' + async with HttpxRequest() as request_client: + response = await request_client.apost(url, data=data) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url + assert request.read().decode() == '{"test":"data"}' @pytest.mark.asyncio -async def test_aput_request(request_client: Request, httpx_mock: HTTPXMock): +async def test_aput_request(httpx_mock: HTTPXMock): url = "https://api.example.com/data" data = {"test": "data"} httpx_mock.add_response(json={"status": "updated"}) - response = await request_client.aput(url, data=data) - request = httpx_mock.get_request() - assert request.url == url - assert request.read().decode() == '{"test": "data"}' + async with HttpxRequest() as request_client: + response = await request_client.aput(url, data=data) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url + assert request.read().decode() == '{"test":"data"}' @pytest.mark.asyncio -async def test_adelete_request(request_client: Request, httpx_mock: HTTPXMock): +async def test_adelete_request(httpx_mock: HTTPXMock): url = "https://api.example.com/data" httpx_mock.add_response(json={"status": "deleted"}) - response = await request_client.adelete(url) - request = httpx_mock.get_request() - assert request.url == url + async with HttpxRequest() as request_client: + response = await request_client.adelete(url) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url def test_context_manager(httpx_mock: HTTPXMock): - with Request() as client: - url = "https://api.example.com/data" - httpx_mock.add_response(json={"key": "value"}) - response = client.get(url) - assert response.json() == {"key": "value"} + httpx_mock.add_response(json={"status": "ok"}) + + with HttpxRequest() as client: + response = client.get("http://test.com") + assert response.status_code == 200 @pytest.mark.asyncio -async def test_async_context_manager(httpx_mock: HTTPXMock): - async with Request() as client: - url = "https://api.example.com/data" - httpx_mock.add_response(json={"key": "value"}) - response = await client.aget(url) - assert response.json() == {"key": "value"} +async def test_async_context_manager_cleanup(httpx_mock: HTTPXMock): + httpx_mock.add_response(json={"status": "ok"}) - -def test_request_with_params(request_client: Request, httpx_mock: HTTPXMock): - url = "https://api.example.com/data" - params = {"query": "test"} - httpx_mock.add_response(json={"result": "success"}) - - response = request_client.get(url, params=params) - request = httpx_mock.get_request() - assert "query=test" in str(request.url) - - -def test_request_with_headers(request_client: Request, httpx_mock: HTTPXMock): - url = "https://api.example.com/data" - headers = {"Authorization": "Bearer token"} - httpx_mock.add_response(json={"result": "success"}) - - response = request_client.get(url, headers=headers) - request = httpx_mock.get_request() - assert request.headers["Authorization"] == "Bearer token" - - -def test_invalid_json_response(request_client: Request, httpx_mock: HTTPXMock): - url = "https://api.example.com/data" - httpx_mock.add_response(text="Invalid JSON") - - response = request_client.get(url) - with pytest.raises(ValueError): - response.json() + async with HttpxRequest() as client: + response = await client.aget("http://test.com") + assert response.status_code == 200 @pytest.mark.asyncio -async def test_invalid_json_response_async( - request_client: Request, httpx_mock: HTTPXMock -): +async def test_request_with_params(httpx_mock: HTTPXMock): url = "https://api.example.com/data" - httpx_mock.add_response(text="Invalid JSON") + params = {"param1": "value1", "param2": "value2"} + httpx_mock.add_response(json={"status": "success"}) - response = await request_client.aget(url) - with pytest.raises(ValueError): - response.json() - - -def test_closed_context_manager_access(httpx_mock: HTTPXMock): - mock_response_json = {"test": "hahaha"} - mock_headers = {"Content-Type": "application/json"} - httpx_mock.add_response( - json=mock_response_json, headers=mock_headers, status_code=200 - ) - - client = Request() - with client: - pass - - response = client.get("https://api.example.com/data") - assert response.is_closed is True - assert response.json() == mock_response_json - assert response.status_code == 200 - assert response.headers["Content-Type"] == "application/json" - assert response.text == '{"test": "hahaha"}' - assert response.request.method == "GET" - assert str(response.request.url) == "https://api.example.com/data" - - response.close() - assert response.is_closed + async with HttpxRequest() as request_client: + response = await request_client.aget(url, params=params) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == f"{url}?param1=value1¶m2=value2" @pytest.mark.asyncio -async def test_closed_context_manager_access_async(httpx_mock: HTTPXMock): - mock_response_json = {"test": "hahaha"} - mock_headers = {"Content-Type": "application/json"} - httpx_mock.add_response( - json=mock_response_json, headers=mock_headers, status_code=200 - ) +async def test_request_with_headers(httpx_mock: HTTPXMock): + url = "https://api.example.com/data" + headers = {"X-Custom-Header": "test"} + httpx_mock.add_response(json={"status": "success"}) - client = Request() - with client: - pass + async with HttpxRequest() as request_client: + response = await request_client.aget(url, headers=headers) + assert response.status_code == 200 + request = cast(HttpxNativeRequest, httpx_mock.get_request()) + assert str(request.url) == url + assert request.headers["X-Custom-Header"] == "test" - response = await client.aget("https://api.example.com/data") - assert response.is_closed is True - assert response.json() == mock_response_json - assert response.status_code == 200 - assert response.headers["Content-Type"] == "application/json" - assert response.text == '{"test": "hahaha"}' - assert response.request.method == "GET" - assert str(response.request.url) == "https://api.example.com/data" - await response.aclose() - assert response.is_closed +@pytest.mark.asyncio +async def test_invalid_json_response(httpx_mock: HTTPXMock): + url = "https://api.example.com/data" + httpx_mock.add_response(content=b"invalid json", status_code=200) + + async with HttpxRequest() as request_client: + response = await request_client.aget(url) + assert response.status_code == 200 + with pytest.raises(Exception): + response.json() + + +@pytest.mark.asyncio +async def test_invalid_json_response_async(httpx_mock: HTTPXMock): + url = "https://api.example.com/data" + httpx_mock.add_response(content=b"invalid json", status_code=200) + + async with HttpxRequest() as request_client: + response = await request_client.aget(url) + assert response.status_code == 200 + with pytest.raises(Exception): + response.json() diff --git a/freedium-library/src/freedium_library/utils/utils/mutable_string.py b/freedium-library/src/freedium_library/utils/utils/mutable_string.py index b0bfe40..bea0415 100644 --- a/freedium-library/src/freedium_library/utils/utils/mutable_string.py +++ b/freedium-library/src/freedium_library/utils/utils/mutable_string.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import List, Union, overload class MutableString: @@ -31,7 +31,13 @@ class MutableString: def __setitem__(self, key: Union[int, slice], value: str) -> None: self._string_list[key] = value - def __getitem__(self, key: int) -> str: + @overload + def __getitem__(self, key: int) -> str: ... + + @overload + def __getitem__(self, key: slice) -> List[str]: ... + + def __getitem__(self, key: Union[int, slice]) -> Union[str, List[str]]: return self._string_list[key] def __str__(self) -> str: diff --git a/freedium-library/src/freedium_library/utils/utils/test_utf_handler.py b/freedium-library/src/freedium_library/utils/utils/test_utf_handler.py index 8a49aa1..8814d89 100644 --- a/freedium-library/src/freedium_library/utils/utils/test_utf_handler.py +++ b/freedium-library/src/freedium_library/utils/utils/test_utf_handler.py @@ -1,33 +1,31 @@ from loguru import logger - -from freedium_library.utils.utils.utf_handler import UTFEncoding, UTFHandler +from freedium_library.utils.utils.utf_handler import ( + UTFEncoding, + UTFHandler, + CharacterMapping, + PositionTracker, +) logger.add("test.log", level="TRACE") def test_multibyte_emoji(): - # Test string with emoji characters - text = "Noah dragged his two printers out from Settings โš™๏ธ < Printers & Scanners ๐Ÿ–จ๏ธ and dropped them in Dock or Desktop, I donโ€™t remember โ€” but you can drag to both the places.'" + text = "Noah dragged his two printers out from Settings โš™๏ธ < Printers & Scanners ๐Ÿ–จ๏ธ and dropped them in Dock or Desktop, I don't remember โ€” but you can drag to both the places.'" - # Initialize handler with UTF-16 encoding handler = UTFHandler(text, UTFEncoding.UTF16) assert str(handler) == text - # Test position mappings around emojis assert handler.get_encoded_position(39) == 39 # Before "Settings โš™๏ธ" assert handler.get_encoded_position(76) == 77 # After "Scanners ๐Ÿ–จ๏ธ" - # Test string slicing around emoji emoji_section = handler[39:76] assert emoji_section == "Settings โš™๏ธ < Printers & Scanners ๐Ÿ–จ๏ธ" - # Test insertion before emoji handler.insert(39, "") handler.insert(77, "") expected = text[:39] + "" + text[39:76] + "" + text[76:] - # expected = expected.replace("๐Ÿ–จ๏ธ", "๐Ÿ–จ") assert bytes(str(handler), "utf-8") == bytes(expected, "utf-8") assert str(handler) == expected @@ -61,11 +59,9 @@ def test_mathematical_monospace(): assert str(handler) == text - # Test encoded positions for mathematical monospace characters assert handler.get_encoded_position(3) == 3 # Position before '๐™—' assert handler.get_encoded_position(5) == 7 # Position after '๐™—' assert handler.get_encoded_position(7) == 11 # Position after '๐™ค' - # assert handler.get_encoded_position(9) == 15 # Position after '๐™ฉ' handler.insert(3, "") handler.insert(11, "") @@ -75,17 +71,14 @@ def test_mathematical_monospace(): def test_utf8_handling(): - # Test with UTF-8 encoded text containing special characters text = "Hello ไธ–็•Œ! ๐ŸŒ" handler = UTFHandler(text, UTFEncoding.UTF8) assert str(handler) == text - # Test position mapping with UTF-8 characters assert handler.get_encoded_position(6) == 6 # Position before 'ไธ–' assert handler.get_encoded_position(8) == 12 # Position after '็•Œ' - # Test insertion around UTF-8 characters handler.insert(6, "") handler.insert(17, "") @@ -94,36 +87,29 @@ def test_utf8_handling(): def test_utf32_handling(): - # Test with UTF-32 encoded text text = "Testing ๐Ÿš€ UTF-32 ๐ŸŽฏ" handler = UTFHandler(text, UTFEncoding.UTF32) assert str(handler) == text - # Test position mapping with UTF-32 characters assert handler.get_encoded_position(8) == 8 # Position before rocket assert handler.get_encoded_position(18) == 18 # Position before target - # Test deletion around UTF-32 characters handler.delete(7, 2) # Delete " ๐Ÿš€ " assert str(handler) == "Testing UTF-32 ๐ŸŽฏ" def test_edge_cases(): - # Test empty string handler = UTFHandler("", UTFEncoding.UTF16) assert str(handler) == "" - # Test string with only special characters handler = UTFHandler("๐ŸŒŸโœจ๐Ÿ’ซ", UTFEncoding.UTF16) assert str(handler) == "๐ŸŒŸโœจ๐Ÿ’ซ" - # Test consecutive insertions handler.insert(0, "<") handler.insert(10, ">") assert str(handler) == "<๐ŸŒŸโœจ๐Ÿ’ซ>" - # Test deletion at boundaries handler.delete(1, 1) # Delete first emoji assert str(handler) == "<โœจ๐Ÿ’ซ>" @@ -132,14 +118,224 @@ def test_position_tracking(): text = "Mix of ASCII and ๆผขๅญ— with ๐ŸŽฎ" handler = UTFHandler(text, UTFEncoding.UTF16) - # Test position tracking before and after modifications original_pos = handler.get_encoded_position(13) # Position before ๆผข handler.insert(13, "[") handler.insert(17, "]") - # Verify position tracking is maintained new_pos = handler.get_encoded_position(14) # Should account for inserted '[' assert new_pos == original_pos + 1 expected = "Mix of ASCII [and] ๆผขๅญ— with ๐ŸŽฎ" assert str(handler) == expected + + +def test_insert_and_getitem(): + original = "Hello World" + handler = UTFHandler(original, UTFEncoding.UTF8) + handler.insert(5, " Amazing") + expected = "Hello Amazing World" + assert str(handler) == expected + assert handler[0] == expected[0] + assert handler[-1] == expected[-1] + + +def test_delete_and_get_slice(): + original = "Hello Amazing World" + handler = UTFHandler(original, UTFEncoding.UTF8) + handler.delete(5, 8) + expected = "Hello World" + assert str(handler) == expected + assert handler.get_string_slice(0, 5) == "Hello" + assert handler[6:] == "World" + + +def test_repr_returns_repr(): + text = "Test string" + handler = UTFHandler(text, UTFEncoding.UTF8) + rep = repr(handler) + assert text in rep + + +def test_character_mapping_negative(): + try: + CharacterMapping( + char="a", + original_pos=-1, + original_encoded_pos=0, + current_pos=0, + encoded_pos=0, + original_char_length=1, + char_length=1, + ) + assert False, "ValueError not raised for negative original_pos" + except ValueError: + pass + try: + CharacterMapping( + char="a", + original_pos=0, + original_encoded_pos=0, + current_pos=-1, + encoded_pos=0, + original_char_length=1, + char_length=1, + ) + assert False, "ValueError not raised for negative current_pos" + except ValueError: + pass + + +def test_character_mapping_char_length(): + try: + CharacterMapping( + char="a", + original_pos=0, + original_encoded_pos=0, + current_pos=0, + encoded_pos=0, + original_char_length=1, + char_length=0, + ) + assert False, "ValueError not raised for char_length less than 1" + except ValueError: + pass + + +def test_shift_positions(): + cm = CharacterMapping( + char="a", + original_pos=0, + original_encoded_pos=0, + current_pos=5, + encoded_pos=10, + original_char_length=1, + char_length=1, + ) + cm.shift_positions(2, 3) + assert cm.current_pos == 7 + assert cm.encoded_pos == 13 + + +def test_position_tracker(): + tracker = PositionTracker() + cm1 = CharacterMapping( + char="a", + original_pos=1, + original_encoded_pos=1, + current_pos=1, + encoded_pos=1, + original_char_length=1, + char_length=1, + ) + cm2 = CharacterMapping( + char="b", + original_pos=5, + original_encoded_pos=5, + current_pos=5, + encoded_pos=5, + original_char_length=1, + char_length=1, + ) + tracker.add(cm1) + tracker.add(cm2) + mappings = tracker.get() + assert len(mappings) == 2 + + cleared = tracker.clear(0, 3) + assert cleared == 1 + mappings = tracker.get() + assert len(mappings) == 1 + assert mappings[0].char == "b" + + cm3 = CharacterMapping( + char="c", + original_pos=10, + original_encoded_pos=10, + current_pos=10, + encoded_pos=10, + original_char_length=1, + char_length=1, + ) + tracker.add(cm3) + tracker.update(3, 2, 3) + mappings = tracker.get() + for mapping in mappings: + if mapping.char == "b": + assert mapping.original_pos == 3 + assert mapping.encoded_pos == 2 + if mapping.char == "c": + assert mapping.original_pos == 8 + assert mapping.encoded_pos == 7 + + +def test_utf_encoding_properties(): + assert hasattr(UTFEncoding, "UTF8") + assert hasattr(UTFEncoding, "UTF16") + assert hasattr(UTFEncoding, "UTF32") + assert hasattr(UTFEncoding, "UTF32") + + +def test_position_tracker_update_edge_cases(): + """Test PositionTracker.update() with various modification scenarios""" + tracker = PositionTracker() + mappings = [ + CharacterMapping( + char="a", + original_pos=5, + original_encoded_pos=10, + current_pos=5, + encoded_pos=10, + original_char_length=1, + char_length=2, + ), + CharacterMapping( + char="b", + original_pos=10, + original_encoded_pos=20, + current_pos=10, + encoded_pos=20, + original_char_length=1, + char_length=3, + ), + ] + for m in mappings: + tracker.add(m) + + # Test update before all mappings + tracker.update(0, 3, 6) + assert tracker.get()[0].original_pos == 2 # 5 - 3 = 2 + assert tracker.get()[0].encoded_pos == 4 # 10 - 6 = 4 + assert tracker.get()[1].original_pos == 7 # 10 - 3 = 7 + assert tracker.get()[1].encoded_pos == 14 # 20 - 6 = 14 + + # Test update overlapping first mapping (should not affect it) + tracker.update(3, 4, 2) + # Only second mapping should be affected + assert tracker.get()[0].original_pos == 2 # Unchanged + assert tracker.get()[0].encoded_pos == 4 # Unchanged + assert tracker.get()[1].original_pos == 3 # 7 - 4 = 3 + assert tracker.get()[1].encoded_pos == 12 # 14 - 2 = 12 + + +def test_full_repr_verification(): + """Test UTFHandler __repr__ contains encoding information""" + text = "Test" + handler = UTFHandler(text, UTFEncoding.UTF16) + rep = repr(handler) + assert "utf-16-le" in rep # Check actual encoding name + assert text in rep + assert "UTFHandler" in rep + + +def test_encoded_position_boundary_conditions(): + """Test position conversion at multi-byte character boundaries""" + text = "a๐Ÿš€b" + handler = UTFHandler(text, UTFEncoding.UTF16) + + # UTF-16 encoded positions: + # 'a' (1 code unit) -> pos 0 + # ๐Ÿš€ (2 code units) -> starts at pos 1 + # 'b' (1 code unit) -> starts at pos 3 + assert handler.get_encoded_position(0) == 0 # 'a' + assert handler.get_encoded_position(1) == 1 # ๐Ÿš€ start + assert handler.get_encoded_position(2) == 3 # 'b' start diff --git a/freedium-library/src/freedium_library/utils/utils/utf_handler.py b/freedium-library/src/freedium_library/utils/utils/utf_handler.py index 759425d..becee15 100644 --- a/freedium-library/src/freedium_library/utils/utils/utf_handler.py +++ b/freedium-library/src/freedium_library/utils/utils/utf_handler.py @@ -1,7 +1,7 @@ from abc import ABC from dataclasses import dataclass from enum import Enum, auto -from typing import List, Optional +from typing import List, Optional, Union, overload from loguru import logger @@ -53,9 +53,11 @@ class CharacterMapping: f"Invalid negative position: original_pos={self.original_pos}, current_pos={self.current_pos}" ) raise ValueError("Positions cannot be negative") - if self.char_length < 1: - logger.error(f"Invalid character length: {self.char_length}") - raise ValueError("Character length must be positive") + if self.char_length < 1 or self.original_char_length < 1: + logger.error( + f"Invalid character lengths: {self.char_length} (current), {self.original_char_length} (original)" + ) + raise ValueError("Character lengths must be positive") logger.trace("CharacterMapping validation successful") def shift_positions(self, offset: int, encoded_offset: int) -> None: @@ -185,7 +187,7 @@ class UTFHandler(ABC): current_pos = 0 encoded_pos = 0 - for i, char in enumerate(self._string): + for i, char in enumerate(str(self._string)): logger.trace(f"Processing character '{char}' at position {i}") encoded_bytes = char.encode(self._encoding.name) char_len = len(encoded_bytes) // self._encoding.unit_size @@ -356,16 +358,26 @@ class UTFHandler(ABC): ) logger.trace(f"Updated string contents: '{self._string}'") - def get_string_slice(self, start: int, end: int) -> List[str]: + def get_string_slice(self, start: int, end: int) -> str: logger.debug(f"Getting string slice from {start} to {end}") - result = list(self._string[start:end]) + result = str(self._string)[start:end] logger.trace(f"Slice result: {result}") return result - def __getitem__(self, key: int) -> str: - logger.debug(f"Getting character at index {key}") - result = "".join(self._string[key]) - logger.trace(f"Retrieved character: '{result}'") + @overload + def __getitem__(self, key: int) -> str: ... + + @overload + def __getitem__(self, key: slice) -> str: ... + + def __getitem__(self, key: Union[int, slice]) -> str: + logger.debug(f"Getting item for key {key}") + item = self._string[key] + if isinstance(item, list): + result = "".join(item) + else: + result = str(item) + logger.trace(f"Retrieved: '{result}'") return result def __str__(self) -> str: @@ -379,13 +391,3 @@ class UTFHandler(ABC): result = f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})" logger.trace(f"Detailed representation: {result}") return result - logger.debug("Converting to string representation") - result = str(self._string) - logger.trace(f"String representation: '{result}'") - return result - - def __repr__(self) -> str: - logger.debug("Getting detailed string representation") - result = f"{self.__class__.__name__}(string='{self.__str__()}', encoding={self._encoding.name})" - logger.trace(f"Detailed representation: {result}") - return result diff --git a/new-web/bun.lockb b/new-web/bun.lockb index 03a7e9d..a4cd36b 100755 Binary files a/new-web/bun.lockb and b/new-web/bun.lockb differ diff --git a/new-web/package.json b/new-web/package.json index 09e7019..c560154 100644 --- a/new-web/package.json +++ b/new-web/package.json @@ -16,29 +16,29 @@ "@iconify-json/heroicons-outline": "^1.2.1", "@iconify-json/mage": "^1.2.2", "@iconify-json/mdi": "^1.2.3", - "@iconify-json/simple-icons": "^1.2.21", + "@iconify-json/simple-icons": "^1.2.37", "@iconify-json/teenyicons": "^1.2.2", - "@shikijs/rehype": "^1.29.1", + "@shikijs/rehype": "^1.29.2", "@sveltejs/adapter-auto": "^3.3.1", - "@sveltejs/kit": "^2.16.1", + "@sveltejs/kit": "^2.21.2", "@sveltejs/vite-plugin-svelte": "^3.1.2", - "autoprefixer": "^10.4.20", + "autoprefixer": "^10.4.21", "clsx": "^2.1.1", "cmdk-sv": "^0.0.18", "lucide-svelte": "^0.453.0", "mode-watcher": "^0.4.1", - "postcss": "^8.5.1", - "prettier": "^3.4.2", - "prettier-plugin-svelte": "^3.3.3", - "svelte": "^4.2.19", - "svelte-check": "^4.1.4", + "postcss": "^8.5.4", + "prettier": "^3.5.3", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^4.2.20", + "svelte-check": "^4.2.1", "svelte-sonner": "^0.3.28", "tailwind-merge": "^2.6.0", "tailwind-variants": "^0.2.1", "tailwindcss": "^3.4.17", - "typescript": "^5.7.3", + "typescript": "^5.8.3", "vaul-svelte": "^0.3.2", - "vite": "^5.4.14" + "vite": "^5.4.19" }, "type": "module", "dependencies": { @@ -47,13 +47,13 @@ "@iconify/tailwind": "^1.2.0", "bits-ui": "^0.21.16", "copy-to-clipboard": "^3.3.3", - "es-toolkit": "^1.31.0", - "hast-util-to-html": "^9.0.4", - "mdsvex": "^0.12.3", + "es-toolkit": "^1.39.1", + "hast-util-to-html": "^9.0.5", + "mdsvex": "^0.12.6", "medium-zoom": "^1.1.0", "ofetch": "^1.4.1", "rehype-external-links": "^3.0.0", - "shiki": "^1.29.1", + "shiki": "^1.29.2", "shiki-transformer-copy-button": "^0.0.3", "svelte-bricks": "^0.2.1", "svelte-lazy": "1.2.9", diff --git a/new-web/src/lib/elements/ArticlePage.svelte b/new-web/src/lib/elements/ArticlePage.svelte index ae0df7e..55aab43 100644 --- a/new-web/src/lib/elements/ArticlePage.svelte +++ b/new-web/src/lib/elements/ArticlePage.svelte @@ -52,7 +52,7 @@

{getErrorMessage(error)}

- {#if error.details && process.env.NODE_ENV === 'development'} + {#if error.details && import.meta.env.DEV}
 					{error.details}
 				
diff --git a/new-web/tsconfig.json b/new-web/tsconfig.json index 91a67a9..941def5 100644 --- a/new-web/tsconfig.json +++ b/new-web/tsconfig.json @@ -1,15 +1,7 @@ { - "baseUrl": ".", "extends": "./.svelte-kit/tsconfig.json", - "paths": { - "$lib": [ - "./src/lib" - ], - "$lib/*": [ - "./src/lib/*" - ] - }, "compilerOptions": { + "baseUrl": ".", "allowJs": true, "checkJs": true, "esModuleInterop": true, @@ -18,7 +10,11 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "paths": { + "$lib": ["./src/lib"], + "$lib/*": ["./src/lib/*"] + } } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files