new frontend: deps update, minor fixes

backend: curl-impersonate as request library option & minor fixes
This commit is contained in:
ZhymabekRoman 2025-06-06 20:18:55 +05:00
parent ce6bdd8d6f
commit 4bf6343292
33 changed files with 1623 additions and 578 deletions

View file

@ -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 <http://www.mongodb.org>"
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"

View file

@ -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",
]

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,
)

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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,

View file

@ -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],
):

View file

@ -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

View file

@ -1 +1,3 @@
productcoalition.com
betterhumans.coach.me
towardsdatascience.com

View file

@ -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}")

View file

@ -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

View file

@ -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

View file

@ -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",
]

View file

@ -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",
)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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})"

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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&param2=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()

View file

@ -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:

View file

@ -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 dont 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, "<code class='test'>")
handler.insert(77, "</code>")
expected = text[:39] + "<code class='test'>" + text[39:76] + "</code>" + 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, "<math>")
handler.insert(11, "</math>")
@ -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, "<zh>")
handler.insert(17, "</zh>")
@ -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

View file

@ -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

Binary file not shown.

View file

@ -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",

View file

@ -52,7 +52,7 @@
<p class="text-gray-600 dark:text-gray-400">
{getErrorMessage(error)}
</p>
{#if error.details && process.env.NODE_ENV === 'development'}
{#if error.details && import.meta.env.DEV}
<pre class="p-4 mt-4 overflow-auto text-sm bg-gray-100 dark:bg-zinc-800">
{error.details}
</pre>

View file

@ -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